detached 0f25ec6

Revert "Implement safe ObjC classes (#157)"

This reverts commit 469fcb91e836003463c0002935c4a383441f3283.

Reason: TotallyGamerJet found an issue (calling the runtime from a different thread?)
commit
0f25ec68926151c8611e07b052cf27417d9a3313
author
Hajime Hoshi · · 2023-09-07 15:11 UTC
parent
469fcb9
signature
unsigned
refs
tag: v0.5.0-alpha.2
3 files · +231 -241
M examples/objc/main_darwin.go +32 -31
@@ -5,7 +5,7 @@ package main
55
66 import (
77 "fmt"
8- "reflect"
8+ "unsafe"
99
1010 "github.com/ebitengine/purego/objc"
1111 )
@@ -17,44 +17,45 @@ var (
1717 sel_bar = objc.RegisterName("bar")
1818 )
1919
20-func BarInit(id objc.ID, cmd objc.SEL) objc.ID {
21- return id.SendSuper(cmd)
20+type barObject struct {
21+ isa objc.Class `objc:"BarObject : NSObject <NSDelegateWindow>"`
22+ bar int
23+}
24+
25+func (b *barObject) Init(_cmd objc.SEL) objc.ID {
26+ return objc.ID(unsafe.Pointer(b)).SendSuper(_cmd)
27+}
28+
29+func (b *barObject) Bar(_cmd objc.SEL) int {
30+ return b.bar
31+}
32+
33+func (b *barObject) SetBar(_cmd objc.SEL, bar int) {
34+ b.bar = bar
35+}
36+
37+func (_ *barObject) Selector(metName string) objc.SEL {
38+ switch metName {
39+ case "Init":
40+ return sel_init
41+ case "SetBar":
42+ return sel_setBar
43+ case "Bar":
44+ return sel_bar
45+ default:
46+ return 0
47+ }
2248 }
2349
2450 func main() {
25- // This struct is equivalent to the following Objective-C definition.
26- //
27- // @interface BarObject : NSObject <NSDelegateWindow>
28- // @property (readwrite) bar int
29- // @end
30- //
31- class, err := objc.RegisterClass(
32- "BarObject",
33- objc.GetClass("NSObject"),
34- []*objc.Protocol{
35- objc.GetProtocol("NSDelegateWindow"),
36- },
37- []objc.FieldDef{
38- {
39- Name: "bar",
40- Type: reflect.TypeOf(int(0)),
41- Attribute: objc.ReadWrite,
42- },
43- },
44- []objc.MethodDef{
45- {
46- Cmd: sel_init,
47- Fn: BarInit,
48- },
49- },
50- )
51+ class, err := objc.RegisterClass(&barObject{})
5152 if err != nil {
5253 panic(err)
5354 }
5455
55- object := objc.ID(class).Send(sel_new)
56+ var object = objc.ID(class).Send(sel_new)
5657 object.Send(sel_setBar, 123)
57- bar := int(object.Send(sel_bar))
58+ var bar = int(object.Send(sel_bar))
5859 fmt.Println(bar)
5960 // Output: 123
6061 }
@@ -5,7 +5,7 @@ package main
5 5
6 import (6 import (
7 "fmt"7 "fmt"
8- "reflect"8+ "unsafe"
9 9
10 "github.com/ebitengine/purego/objc"10 "github.com/ebitengine/purego/objc"
11 )11 )
@@ -17,44 +17,45 @@ var (
17 sel_bar = objc.RegisterName("bar")17 sel_bar = objc.RegisterName("bar")
18 )18 )
19 19
20-func BarInit(id objc.ID, cmd objc.SEL) objc.ID {20+type barObject struct {
21- return id.SendSuper(cmd)21+ isa objc.Class `objc:"BarObject : NSObject <NSDelegateWindow>"`
22+ bar int
23+}
24+
25+func (b *barObject) Init(_cmd objc.SEL) objc.ID {
26+ return objc.ID(unsafe.Pointer(b)).SendSuper(_cmd)
27+}
28+
29+func (b *barObject) Bar(_cmd objc.SEL) int {
30+ return b.bar
31+}
32+
33+func (b *barObject) SetBar(_cmd objc.SEL, bar int) {
34+ b.bar = bar
35+}
36+
37+func (_ *barObject) Selector(metName string) objc.SEL {
38+ switch metName {
39+ case "Init":
40+ return sel_init
41+ case "SetBar":
42+ return sel_setBar
43+ case "Bar":
44+ return sel_bar
45+ default:
46+ return 0
47+ }
22 }48 }
23 49
24 func main() {50 func main() {
25- // This struct is equivalent to the following Objective-C definition.51+ class, err := objc.RegisterClass(&barObject{})
26- //
27- // @interface BarObject : NSObject <NSDelegateWindow>
28- // @property (readwrite) bar int
29- // @end
30- //
31- class, err := objc.RegisterClass(
32- "BarObject",
33- objc.GetClass("NSObject"),
34- []*objc.Protocol{
35- objc.GetProtocol("NSDelegateWindow"),
36- },
37- []objc.FieldDef{
38- {
39- Name: "bar",
40- Type: reflect.TypeOf(int(0)),
41- Attribute: objc.ReadWrite,
42- },
43- },
44- []objc.MethodDef{
45- {
46- Cmd: sel_init,
47- Fn: BarInit,
48- },
49- },
50- )
51 if err != nil {52 if err != nil {
52 panic(err)53 panic(err)
53 }54 }
54 55
55- object := objc.ID(class).Send(sel_new)56+ var object = objc.ID(class).Send(sel_new)
56 object.Send(sel_setBar, 123)57 object.Send(sel_setBar, 123)
57- bar := int(object.Send(sel_bar))58+ var bar = int(object.Send(sel_bar))
58 fmt.Println(bar)59 fmt.Println(bar)
59 // Output: 12360 // Output: 123
60 }61 }
M objc/objc_runtime_darwin.go +180 -169
@@ -10,15 +10,13 @@ import (
1010 "fmt"
1111 "math"
1212 "reflect"
13- "regexp"
14- "unicode"
15- "unsafe"
1613
1714 "github.com/ebitengine/purego"
1815 )
1916
20-// TODO: support try/catch?
21-// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
17+//TODO: support try/catch?
18+//https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
19+
2220 var (
2321 objc_msgSend_fn uintptr
2422 objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID
@@ -37,10 +35,6 @@ var (
3735 class_addProtocol func(class Class, protocol *Protocol) bool
3836 ivar_getOffset func(ivar Ivar) uintptr
3937 object_getClass func(obj ID) Class
40- object_getIvar func(obj ID, ivar Ivar) ID
41- object_setIvar func(obj ID, ivar Ivar, value ID)
42- protocol_getName func(protocol *Protocol) string
43- protocol_isEqual func(p *Protocol, p2 *Protocol) bool
4438 )
4539
4640 func init() {
@@ -71,10 +65,6 @@ func init() {
7165 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
7266 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
7367 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
74- purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
75- purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
76- purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
77- purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
7868 }
7969
8070 // ID is an opaque pointer to some Objective-C object
@@ -92,16 +82,6 @@ func (id ID) Send(sel SEL, args ...interface{}) ID {
9282 return objc_msgSend(id, sel, args...)
9383 }
9484
95-// GetIvar reads the value of an instance variable in an object.
96-func (id ID) GetIvar(ivar Ivar) ID {
97- return object_getIvar(id, ivar)
98-}
99-
100-// SetIvar sets the value of an instance variable in an object.
101-func (id ID) SetIvar(ivar Ivar, value ID) {
102- object_setIvar(id, ivar, value)
103-}
104-
10585 // Send is a convenience method for sending messages to objects that can return any type.
10686 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
10787 // It is best to cache the result of RegisterName.
@@ -123,7 +103,7 @@ type objc_super struct {
123103 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
124104 // of RegisterName.
125105 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
126- super := &objc_super{
106+ var super = &objc_super{
127107 receiver: id,
128108 superClass: id.Class(),
129109 }
@@ -134,7 +114,7 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
134114 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
135115 // It is best to cache the result of RegisterName.
136116 func SendSuper[T any](id ID, sel SEL, args ...any) T {
137- super := &objc_super{
117+ var super = &objc_super{
138118 receiver: id,
139119 superClass: id.Class(),
140120 }
@@ -168,171 +148,203 @@ func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
168148 return objc_allocateClassPair(super, name, extraBytes)
169149 }
170150
171-// MethodDef represents the Go function and the selector that ObjC uses to access that function.
172-type MethodDef struct {
173- Cmd SEL
174- Fn any
151+// Selector is an interface that takes a Go method name
152+// and returns the selector equivalent name.
153+// If it returns a nil SEL then that method
154+// is not added to the class object.
155+type Selector interface {
156+ Selector(string) SEL
175157 }
176158
177-// IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically
178-// generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes.
179-// The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib.
180-//
181-// Take for example this Objective-C code:
182-//
183-// @property (readwrite) float value;
184-//
185-// In Go, the functions can be accessed as followed:
186-//
187-// var value = purego.Send[float32](id, purego.RegisterName("value"))
188-// id.Send(purego.RegisterName("setValue:"), 3.46)
189-//
190-// [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html
191-type IvarAttrib int
159+// TagFormatError occurs when the parser fails to parse the objc tag in a Selector object
160+var TagFormatError = errors.New(`objc tag doesn't match "ClassName : SuperClassName <Protocol, ...>""`)
192161
193-const (
194- ReadOnly IvarAttrib = 1 << iota
195- ReadWrite
196-)
162+// MismatchError occurs when the Go struct definition doesn't match that of Objective-C
163+var MismatchError = errors.New("go struct doesn't match objective-c struct")
197164
198-// FieldDef is a definition of a field to add to an Objective-C class.
199-// The name of the field is what will be used to access it through the Ivar. If the type is bool
200-// the name cannot start with `is` since a getter will be generated with the name `isBoolName`.
201-// The name also cannot contain any spaces.
202-// The type is the Go equivalent type of the Ivar.
203-// Attribute determines if a getter and or setter method is generated for this field.
204-type FieldDef struct {
205- Name string
206- Type reflect.Type
207- Attribute IvarAttrib
208-}
165+// RegisterClass takes a pointer to a struct that implements the Selector interface.
166+// It will register the structs fields and pointer receiver methods in the Objective-C
167+// runtime using the SEL returned from Selector. Any errors that occur trying to add
168+// a Method or Ivar is returned as an error. Such errors may occur in parsing or because
169+// the size of the struct does not match the size in Objective-C. If no errors occur
170+// then the returned Class has been registered successfully.
171+//
172+// The struct's first field must be of type Class and have a tag that matches the format
173+// `objc:"ClassName : SuperClassName <Protocol, ...>`. This tag is equal to how the class
174+// would be defined in Objective-C.
175+func RegisterClass(object Selector) (Class, error) {
176+ ptr := reflect.TypeOf(object)
177+ strct := ptr.Elem()
178+ if strct.NumField() == 0 || strct.Field(0).Type != reflect.TypeOf(Class(0)) {
179+ return 0, fmt.Errorf("objc: need objc.Class as first field: %w", MismatchError)
180+ }
181+ isa := strct.Field(0)
182+ tag := isa.Tag.Get("objc")
183+ if tag == "" {
184+ return 0, fmt.Errorf("objc: missing objc tag: %w", TagFormatError)
185+ }
186+ // split contains the class name and super class name followed by all the Protocols
187+ // start with two for ClassName : SuperClassName
188+ var split = make([]string, 2)
189+ {
190+ // This is a simple parser for the objc tag that looks for the format
191+ // "ClassName : SuperClassName <Protocol, ...>"
192+ // It appends to the split variable with the [ClassName, SuperClassName, Protocol, ...]
193+
194+ var i int // from tag[0:i] is whatever identifier is next
195+ var r rune // r is the current rune
196+ skipSpace := func() {
197+ for _, c := range tag {
198+ if c == ' ' {
199+ tag = tag[1:]
200+ continue
201+ }
202+ break
203+ }
204+ }
205+ skipSpace()
206+ // get ClassName
207+ for i, r = range tag {
208+ if r == ' ' || r == ':' {
209+ break
210+ }
211+ }
212+ split[0] = tag[0:i] // store ClassName
213+ tag = tag[i:]
209214
210-// ivarRegex checks to make sure the Ivar is correctly formatted
211-var ivarRegex = regexp.MustCompile("[a-z][a-zA-Z0-9]*")
215+ skipSpace()
212216
213-// RegisterClass takes the name of the class to create, the superclass, a list of protocols this class
214-// implements, a list of fields this class has and a list of methods. It returns the created class or an error
215-// describing what went wrong.
216-func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {
217- class := objc_allocateClassPair(superClass, name, 0)
217+ // check for ':'
218+ if len(tag) > 0 && tag[0] != ':' {
219+ return 0, fmt.Errorf("objc: missing ':': %w", TagFormatError)
220+ }
221+ tag = tag[1:] // skip ':'
222+ skipSpace()
223+
224+ // get SuperClassName
225+ for i, r = range tag {
226+ if r == ' ' {
227+ break
228+ } else if i+1 == len(tag) {
229+ // if this is the last character in the string
230+ // make sure to increment i so that tag[:i]
231+ // includes the last character
232+ i++
233+ break
234+ }
235+ }
236+ if len(tag) < i {
237+ return 0, fmt.Errorf("objc: missing SuperClassName: %w", TagFormatError)
238+ }
239+ split[1] = tag[:i] // store SuperClassName
240+ tag = tag[i:] // drop SuperClassName
241+ skipSpace()
242+ if len(tag) > 0 {
243+ if tag[0] != '<' {
244+ return 0, fmt.Errorf("objc: expected '<': %w", TagFormatError)
245+ }
246+ tag = tag[1:] // drop '<'
247+ // get Protocols
248+ outer:
249+ for {
250+ skipSpace()
251+ for i, r = range tag {
252+ switch r {
253+ case ' ':
254+ split = append(split, tag[:i])
255+ tag = tag[i:]
256+ continue outer
257+ case ',':
258+ // If there is actually an identifier - add it.
259+ if i > 0 {
260+ split = append(split, tag[:i])
261+ tag = tag[i:]
262+ } else {
263+ // Otherwise, drop ','
264+ tag = tag[1:]
265+ }
266+ continue outer
267+ case '>':
268+ // If there is actually an identifier - add it.
269+ if i > 0 {
270+ split = append(split, tag[:i])
271+ tag = tag[i:]
272+ }
273+ break outer
274+ }
275+ }
276+ return 0, fmt.Errorf("objc: expected '>': %w", TagFormatError)
277+ }
278+ }
279+ }
280+ class := objc_allocateClassPair(GetClass(split[1]), split[0], 0)
218281 if class == 0 {
219- return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
282+ return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])
220283 }
221- // Add Protocols
222- for _, p := range protocols {
223- if !class.AddProtocol(p) {
224- return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p))
284+ if len(split) > 2 {
285+ // Add Protocols
286+ for _, n := range split[2:] {
287+ if !class.AddProtocol(GetProtocol(n)) {
288+ return 0, fmt.Errorf("objc: couldn't add Protocol %s", n)
289+ }
225290 }
226291 }
227- // Add exported methods based on the selectors returned from ClassDef(string) SEL
228- for idx, def := range methods {
292+ // Add exported methods based on the selectors returned from Selector(string) SEL
293+ for i := 0; i < ptr.NumMethod(); i++ {
294+ met := ptr.Method(i)
295+ // we know this method is the interface one since RegisterClass
296+ // requires that the struct implement Selector.
297+ if met.Name == "Selector" {
298+ continue
299+ }
300+ sel := object.Selector(met.Name)
301+ if sel == 0 {
302+ continue
303+ }
304+ fn := met.Func.Interface()
229305 imp, err := func() (imp IMP, err error) {
230306 defer func() {
231307 if r := recover(); r != nil {
232308 err = fmt.Errorf("objc: failed to create IMP: %s", r)
233309 }
234310 }()
235- return NewIMP(def.Fn), nil
311+ return NewIMP(fn), nil
236312 }()
237313 if err != nil {
238- return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
314+ return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
239315 }
240- encoding, err := encodeFunc(def.Fn)
316+ encoding, err := encodeFunc(fn)
241317 if err != nil {
242- return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
318+ return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
243319 }
244- if !class.AddMethod(def.Cmd, imp, encoding) {
245- return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
320+ if !class.AddMethod(sel, imp, encoding) {
321+ return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)
246322 }
247323 }
248324 // Add Ivars
249- for _, ivar := range ivars {
250- if !ivarRegex.MatchString(ivar.Name) {
251- return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name)
325+ // Start at 1 because we skip the class object which is first
326+ for i := 1; i < strct.NumField(); i++ {
327+ f := strct.Field(i)
328+ if f.Name == "_" {
329+ continue
252330 }
253- size := ivar.Type.Size()
254- alignment := uint8(math.Log2(float64(ivar.Type.Align())))
255- enc, err := encodeType(ivar.Type, false)
331+ size := f.Type.Size()
332+ alignment := uint8(math.Log2(float64(f.Type.Align())))
333+ enc, err := encodeType(f.Type, false)
256334 if err != nil {
257- return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
335+ return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)
258336 }
259- if !class_addIvar(class, ivar.Name, size, alignment, enc) {
260- return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
337+ if !class_addIvar(class, f.Name, size, alignment, enc) {
338+ return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)
261339 }
262- switch ivar.Attribute {
263- case ReadWrite:
264- ty := reflect.FuncOf(
265- []reflect.Type{
266- reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type,
267- },
268- nil, false,
269- )
270- var encoding string
271- if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
272- return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
273- }
274- instanceVariable := class.InstanceVariable(ivar.Name)
275- val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
276- // on entry the first and second arguments are ID and SEL followed by the value
277- if len(args) != 3 {
278- panic(fmt.Errorf("objc: incorrect number of arguments"))
279- }
280- // Grab the pointer from using the Ivar and dereference it
281- var value ID
282- switch args[2].Kind() {
283- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
284- value = ID(args[2].Int())
285- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
286- value = ID(args[2].Uint())
287- case reflect.Float32, reflect.Float64:
288- value = ID(math.Float64bits(args[2].Float()))
289- case reflect.Bool:
290- if args[2].Bool() {
291- value = 1
292- }
293- case reflect.Pointer, reflect.UnsafePointer:
294- value = ID(args[2].Pointer())
295- default:
296- panic(fmt.Errorf("objc: unsupported kind %s", args[2].Kind()))
297- }
298- object_setIvar(args[0].Interface().(ID), instanceVariable, value)
299- return nil
300- }).Interface()
301- // this code only works for ascii but that shouldn't be a problem
302- selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00"
303- class.AddMethod(RegisterName(selector), NewIMP(val), encoding)
304- fallthrough // also implement the read method
305- case ReadOnly:
306- ty := reflect.FuncOf(
307- []reflect.Type{
308- reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)),
309- },
310- []reflect.Type{ivar.Type}, false,
311- )
312- var encoding string
313- if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
314- return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
315- }
316- instanceVariable := class.InstanceVariable(ivar.Name)
317- val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
318- // on entry the first and second arguments are ID and SEL
319- if len(args) != 2 {
320- panic(fmt.Errorf("objc: incorrect number of arguments"))
321- }
322- // Grab the pointer from using the Ivar and dereference it
323- variable := object_getIvar(args[0].Interface().(ID), instanceVariable)
324- return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Pointer(&variable)).Elem()}
325- }).Interface()
326- if ivar.Type.Kind() == reflect.Bool {
327- // this code only works for ascii but that shouldn't be a problem
328- ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:]
329- }
330- class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding)
331- default:
332- return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute)
340+ if offset := class.InstanceVariable(f.Name).Offset(); offset != f.Offset {
341+ return 0, fmt.Errorf("objc: couldn't add Ivar %s because offset (%d != %d)", f.Name, offset, f.Offset)
333342 }
334343 }
335344 objc_registerClassPair(class)
345+ if size1, size2 := class.InstanceSize(), strct.Size(); size1 != size2 {
346+ return 0, fmt.Errorf("objc: sizes don't match %d != %d: %w", size1, size2, MismatchError)
347+ }
336348 return class, nil
337349 }
338350
@@ -408,7 +420,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
408420 if insidePtr {
409421 return encStructBegin + typ.Name() + encStructEnd, nil
410422 }
411- encoding := encStructBegin
423+ var encoding = encStructBegin
412424 encoding += typ.Name()
413425 encoding += "="
414426 for i := 0; i < typ.NumField(); i++ {
@@ -532,24 +544,19 @@ func (i Ivar) Offset() uintptr {
532544 }
533545
534546 // Protocol is a type that declares methods that can be implemented by any class.
535-type Protocol [0]func()
547+type Protocol uintptr
536548
537549 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
538550 func GetProtocol(name string) *Protocol {
539551 return objc_getProtocol(name)
540552 }
541553
542-// Equals return true if the two protocols are the same.
543-func (p *Protocol) Equals(p2 *Protocol) bool {
544- return protocol_isEqual(p, p2)
545-}
546-
547554 // IMP is a function pointer that can be called by Objective-C code.
548555 type IMP uintptr
549556
550557 // NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
558+// ID may instead be a pointer to a struct whose first field has type Class.
551559 // It returns an IMP function pointer that can be called by Objective-C code.
552-// The function panics if an error occurs.
553560 // The function pointer is never deallocated.
554561 func NewIMP(fn interface{}) IMP {
555562 ty := reflect.TypeOf(fn)
@@ -561,9 +568,13 @@ func NewIMP(fn interface{}) IMP {
561568 switch {
562569 case ty.NumIn() < 2:
563570 fallthrough
564- case ty.In(0) != reflect.TypeOf(ID(0)):
571+ case ty.In(0).Kind() != reflect.Uintptr && // checks if it's objc.ID
572+ // or that it's a pointer to a struct
573+ (ty.In(0).Kind() != reflect.Pointer || ty.In(0).Elem().Kind() != reflect.Struct ||
574+ // and that the structs first field is an objc.Class
575+ ty.In(0).Elem().Field(0).Type != reflect.TypeOf(Class(0))):
565576 fallthrough
566- case ty.In(1) != reflect.TypeOf(SEL(0)):
577+ case ty.In(1).Kind() != reflect.Uintptr:
567578 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
568579 }
569580 return IMP(purego.NewCallback(fn))
@@ -10,15 +10,13 @@ import (
10 "fmt"10 "fmt"
11 "math"11 "math"
12 "reflect"12 "reflect"
13- "regexp"
14- "unicode"
15- "unsafe"
16 13
17 "github.com/ebitengine/purego"14 "github.com/ebitengine/purego"
18 )15 )
19 16
20-// TODO: support try/catch?17+//TODO: support try/catch?
21-// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime18+//https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
19+
22 var (20 var (
23 objc_msgSend_fn uintptr21 objc_msgSend_fn uintptr
24 objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID22 objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID
@@ -37,10 +35,6 @@ var (
37 class_addProtocol func(class Class, protocol *Protocol) bool35 class_addProtocol func(class Class, protocol *Protocol) bool
38 ivar_getOffset func(ivar Ivar) uintptr36 ivar_getOffset func(ivar Ivar) uintptr
39 object_getClass func(obj ID) Class37 object_getClass func(obj ID) Class
40- object_getIvar func(obj ID, ivar Ivar) ID
41- object_setIvar func(obj ID, ivar Ivar, value ID)
42- protocol_getName func(protocol *Protocol) string
43- protocol_isEqual func(p *Protocol, p2 *Protocol) bool
44 )38 )
45 39
46 func init() {40 func init() {
@@ -71,10 +65,6 @@ func init() {
71 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")65 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
72 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")66 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
73 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")67 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
74- purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
75- purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
76- purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
77- purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
78 }68 }
79 69
80 // ID is an opaque pointer to some Objective-C object70 // ID is an opaque pointer to some Objective-C object
@@ -92,16 +82,6 @@ func (id ID) Send(sel SEL, args ...interface{}) ID {
92 return objc_msgSend(id, sel, args...)82 return objc_msgSend(id, sel, args...)
93 }83 }
94 84
95-// GetIvar reads the value of an instance variable in an object.
96-func (id ID) GetIvar(ivar Ivar) ID {
97- return object_getIvar(id, ivar)
98-}
99-
100-// SetIvar sets the value of an instance variable in an object.
101-func (id ID) SetIvar(ivar Ivar, value ID) {
102- object_setIvar(id, ivar, value)
103-}
104-
105 // Send is a convenience method for sending messages to objects that can return any type.85 // Send is a convenience method for sending messages to objects that can return any type.
106 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.86 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
107 // It is best to cache the result of RegisterName.87 // It is best to cache the result of RegisterName.
@@ -123,7 +103,7 @@ type objc_super struct {
123 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result103 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
124 // of RegisterName.104 // of RegisterName.
125 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {105 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
126- super := &objc_super{106+ var super = &objc_super{
127 receiver: id,107 receiver: id,
128 superClass: id.Class(),108 superClass: id.Class(),
129 }109 }
@@ -134,7 +114,7 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
134 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.114 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
135 // It is best to cache the result of RegisterName.115 // It is best to cache the result of RegisterName.
136 func SendSuper[T any](id ID, sel SEL, args ...any) T {116 func SendSuper[T any](id ID, sel SEL, args ...any) T {
137- super := &objc_super{117+ var super = &objc_super{
138 receiver: id,118 receiver: id,
139 superClass: id.Class(),119 superClass: id.Class(),
140 }120 }
@@ -168,171 +148,203 @@ func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
168 return objc_allocateClassPair(super, name, extraBytes)148 return objc_allocateClassPair(super, name, extraBytes)
169 }149 }
170 150
171-// MethodDef represents the Go function and the selector that ObjC uses to access that function.151+// Selector is an interface that takes a Go method name
172-type MethodDef struct {152+// and returns the selector equivalent name.
173- Cmd SEL153+// If it returns a nil SEL then that method
174- Fn any154+// is not added to the class object.
155+type Selector interface {
156+ Selector(string) SEL
175 }157 }
176 158
177-// IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically159+// TagFormatError occurs when the parser fails to parse the objc tag in a Selector object
178-// generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes.160+var TagFormatError = errors.New(`objc tag doesn't match "ClassName : SuperClassName <Protocol, ...>""`)
179-// The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib.
180-//
181-// Take for example this Objective-C code:
182-//
183-// @property (readwrite) float value;
184-//
185-// In Go, the functions can be accessed as followed:
186-//
187-// var value = purego.Send[float32](id, purego.RegisterName("value"))
188-// id.Send(purego.RegisterName("setValue:"), 3.46)
189-//
190-// [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html
191-type IvarAttrib int
192 161
193-const (162+// MismatchError occurs when the Go struct definition doesn't match that of Objective-C
194- ReadOnly IvarAttrib = 1 << iota163+var MismatchError = errors.New("go struct doesn't match objective-c struct")
195- ReadWrite
196-)
197 164
198-// FieldDef is a definition of a field to add to an Objective-C class.165+// RegisterClass takes a pointer to a struct that implements the Selector interface.
199-// The name of the field is what will be used to access it through the Ivar. If the type is bool166+// It will register the structs fields and pointer receiver methods in the Objective-C
200-// the name cannot start with `is` since a getter will be generated with the name `isBoolName`.167+// runtime using the SEL returned from Selector. Any errors that occur trying to add
201-// The name also cannot contain any spaces.168+// a Method or Ivar is returned as an error. Such errors may occur in parsing or because
202-// The type is the Go equivalent type of the Ivar.169+// the size of the struct does not match the size in Objective-C. If no errors occur
203-// Attribute determines if a getter and or setter method is generated for this field.170+// then the returned Class has been registered successfully.
204-type FieldDef struct {171+//
205- Name string172+// The struct's first field must be of type Class and have a tag that matches the format
206- Type reflect.Type173+// `objc:"ClassName : SuperClassName <Protocol, ...>`. This tag is equal to how the class
207- Attribute IvarAttrib174+// would be defined in Objective-C.
208-}175+func RegisterClass(object Selector) (Class, error) {
176+ ptr := reflect.TypeOf(object)
177+ strct := ptr.Elem()
178+ if strct.NumField() == 0 || strct.Field(0).Type != reflect.TypeOf(Class(0)) {
179+ return 0, fmt.Errorf("objc: need objc.Class as first field: %w", MismatchError)
180+ }
181+ isa := strct.Field(0)
182+ tag := isa.Tag.Get("objc")
183+ if tag == "" {
184+ return 0, fmt.Errorf("objc: missing objc tag: %w", TagFormatError)
185+ }
186+ // split contains the class name and super class name followed by all the Protocols
187+ // start with two for ClassName : SuperClassName
188+ var split = make([]string, 2)
189+ {
190+ // This is a simple parser for the objc tag that looks for the format
191+ // "ClassName : SuperClassName <Protocol, ...>"
192+ // It appends to the split variable with the [ClassName, SuperClassName, Protocol, ...]
193+
194+ var i int // from tag[0:i] is whatever identifier is next
195+ var r rune // r is the current rune
196+ skipSpace := func() {
197+ for _, c := range tag {
198+ if c == ' ' {
199+ tag = tag[1:]
200+ continue
201+ }
202+ break
203+ }
204+ }
205+ skipSpace()
206+ // get ClassName
207+ for i, r = range tag {
208+ if r == ' ' || r == ':' {
209+ break
210+ }
211+ }
212+ split[0] = tag[0:i] // store ClassName
213+ tag = tag[i:]
209 214
210-// ivarRegex checks to make sure the Ivar is correctly formatted215+ skipSpace()
211-var ivarRegex = regexp.MustCompile("[a-z][a-zA-Z0-9]*")
212 216
213-// RegisterClass takes the name of the class to create, the superclass, a list of protocols this class217+ // check for ':'
214-// implements, a list of fields this class has and a list of methods. It returns the created class or an error218+ if len(tag) > 0 && tag[0] != ':' {
215-// describing what went wrong.219+ return 0, fmt.Errorf("objc: missing ':': %w", TagFormatError)
216-func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {220+ }
217- class := objc_allocateClassPair(superClass, name, 0)221+ tag = tag[1:] // skip ':'
222+ skipSpace()
223+
224+ // get SuperClassName
225+ for i, r = range tag {
226+ if r == ' ' {
227+ break
228+ } else if i+1 == len(tag) {
229+ // if this is the last character in the string
230+ // make sure to increment i so that tag[:i]
231+ // includes the last character
232+ i++
233+ break
234+ }
235+ }
236+ if len(tag) < i {
237+ return 0, fmt.Errorf("objc: missing SuperClassName: %w", TagFormatError)
238+ }
239+ split[1] = tag[:i] // store SuperClassName
240+ tag = tag[i:] // drop SuperClassName
241+ skipSpace()
242+ if len(tag) > 0 {
243+ if tag[0] != '<' {
244+ return 0, fmt.Errorf("objc: expected '<': %w", TagFormatError)
245+ }
246+ tag = tag[1:] // drop '<'
247+ // get Protocols
248+ outer:
249+ for {
250+ skipSpace()
251+ for i, r = range tag {
252+ switch r {
253+ case ' ':
254+ split = append(split, tag[:i])
255+ tag = tag[i:]
256+ continue outer
257+ case ',':
258+ // If there is actually an identifier - add it.
259+ if i > 0 {
260+ split = append(split, tag[:i])
261+ tag = tag[i:]
262+ } else {
263+ // Otherwise, drop ','
264+ tag = tag[1:]
265+ }
266+ continue outer
267+ case '>':
268+ // If there is actually an identifier - add it.
269+ if i > 0 {
270+ split = append(split, tag[:i])
271+ tag = tag[i:]
272+ }
273+ break outer
274+ }
275+ }
276+ return 0, fmt.Errorf("objc: expected '>': %w", TagFormatError)
277+ }
278+ }
279+ }
280+ class := objc_allocateClassPair(GetClass(split[1]), split[0], 0)
218 if class == 0 {281 if class == 0 {
219- return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)282+ return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])
220 }283 }
221- // Add Protocols284+ if len(split) > 2 {
222- for _, p := range protocols {285+ // Add Protocols
223- if !class.AddProtocol(p) {286+ for _, n := range split[2:] {
224- return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p))287+ if !class.AddProtocol(GetProtocol(n)) {
288+ return 0, fmt.Errorf("objc: couldn't add Protocol %s", n)
289+ }
225 }290 }
226 }291 }
227- // Add exported methods based on the selectors returned from ClassDef(string) SEL292+ // Add exported methods based on the selectors returned from Selector(string) SEL
228- for idx, def := range methods {293+ for i := 0; i < ptr.NumMethod(); i++ {
294+ met := ptr.Method(i)
295+ // we know this method is the interface one since RegisterClass
296+ // requires that the struct implement Selector.
297+ if met.Name == "Selector" {
298+ continue
299+ }
300+ sel := object.Selector(met.Name)
301+ if sel == 0 {
302+ continue
303+ }
304+ fn := met.Func.Interface()
229 imp, err := func() (imp IMP, err error) {305 imp, err := func() (imp IMP, err error) {
230 defer func() {306 defer func() {
231 if r := recover(); r != nil {307 if r := recover(); r != nil {
232 err = fmt.Errorf("objc: failed to create IMP: %s", r)308 err = fmt.Errorf("objc: failed to create IMP: %s", r)
233 }309 }
234 }()310 }()
235- return NewIMP(def.Fn), nil311+ return NewIMP(fn), nil
236 }()312 }()
237 if err != nil {313 if err != nil {
238- return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)314+ return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
239 }315 }
240- encoding, err := encodeFunc(def.Fn)316+ encoding, err := encodeFunc(fn)
241 if err != nil {317 if err != nil {
242- return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)318+ return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
243 }319 }
244- if !class.AddMethod(def.Cmd, imp, encoding) {320+ if !class.AddMethod(sel, imp, encoding) {
245- return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)321+ return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)
246 }322 }
247 }323 }
248 // Add Ivars324 // Add Ivars
249- for _, ivar := range ivars {325+ // Start at 1 because we skip the class object which is first
250- if !ivarRegex.MatchString(ivar.Name) {326+ for i := 1; i < strct.NumField(); i++ {
251- return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name)327+ f := strct.Field(i)
328+ if f.Name == "_" {
329+ continue
252 }330 }
253- size := ivar.Type.Size()331+ size := f.Type.Size()
254- alignment := uint8(math.Log2(float64(ivar.Type.Align())))332+ alignment := uint8(math.Log2(float64(f.Type.Align())))
255- enc, err := encodeType(ivar.Type, false)333+ enc, err := encodeType(f.Type, false)
256 if err != nil {334 if err != nil {
257- return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)335+ return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)
258 }336 }
259- if !class_addIvar(class, ivar.Name, size, alignment, enc) {337+ if !class_addIvar(class, f.Name, size, alignment, enc) {
260- return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)338+ return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)
261 }339 }
262- switch ivar.Attribute {340+ if offset := class.InstanceVariable(f.Name).Offset(); offset != f.Offset {
263- case ReadWrite:341+ return 0, fmt.Errorf("objc: couldn't add Ivar %s because offset (%d != %d)", f.Name, offset, f.Offset)
264- ty := reflect.FuncOf(
265- []reflect.Type{
266- reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type,
267- },
268- nil, false,
269- )
270- var encoding string
271- if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
272- return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
273- }
274- instanceVariable := class.InstanceVariable(ivar.Name)
275- val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
276- // on entry the first and second arguments are ID and SEL followed by the value
277- if len(args) != 3 {
278- panic(fmt.Errorf("objc: incorrect number of arguments"))
279- }
280- // Grab the pointer from using the Ivar and dereference it
281- var value ID
282- switch args[2].Kind() {
283- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
284- value = ID(args[2].Int())
285- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
286- value = ID(args[2].Uint())
287- case reflect.Float32, reflect.Float64:
288- value = ID(math.Float64bits(args[2].Float()))
289- case reflect.Bool:
290- if args[2].Bool() {
291- value = 1
292- }
293- case reflect.Pointer, reflect.UnsafePointer:
294- value = ID(args[2].Pointer())
295- default:
296- panic(fmt.Errorf("objc: unsupported kind %s", args[2].Kind()))
297- }
298- object_setIvar(args[0].Interface().(ID), instanceVariable, value)
299- return nil
300- }).Interface()
301- // this code only works for ascii but that shouldn't be a problem
302- selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00"
303- class.AddMethod(RegisterName(selector), NewIMP(val), encoding)
304- fallthrough // also implement the read method
305- case ReadOnly:
306- ty := reflect.FuncOf(
307- []reflect.Type{
308- reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)),
309- },
310- []reflect.Type{ivar.Type}, false,
311- )
312- var encoding string
313- if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
314- return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
315- }
316- instanceVariable := class.InstanceVariable(ivar.Name)
317- val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
318- // on entry the first and second arguments are ID and SEL
319- if len(args) != 2 {
320- panic(fmt.Errorf("objc: incorrect number of arguments"))
321- }
322- // Grab the pointer from using the Ivar and dereference it
323- variable := object_getIvar(args[0].Interface().(ID), instanceVariable)
324- return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Pointer(&variable)).Elem()}
325- }).Interface()
326- if ivar.Type.Kind() == reflect.Bool {
327- // this code only works for ascii but that shouldn't be a problem
328- ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:]
329- }
330- class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding)
331- default:
332- return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute)
333 }342 }
334 }343 }
335 objc_registerClassPair(class)344 objc_registerClassPair(class)
345+ if size1, size2 := class.InstanceSize(), strct.Size(); size1 != size2 {
346+ return 0, fmt.Errorf("objc: sizes don't match %d != %d: %w", size1, size2, MismatchError)
347+ }
336 return class, nil348 return class, nil
337 }349 }
338 350
@@ -408,7 +420,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
408 if insidePtr {420 if insidePtr {
409 return encStructBegin + typ.Name() + encStructEnd, nil421 return encStructBegin + typ.Name() + encStructEnd, nil
410 }422 }
411- encoding := encStructBegin423+ var encoding = encStructBegin
412 encoding += typ.Name()424 encoding += typ.Name()
413 encoding += "="425 encoding += "="
414 for i := 0; i < typ.NumField(); i++ {426 for i := 0; i < typ.NumField(); i++ {
@@ -532,24 +544,19 @@ func (i Ivar) Offset() uintptr {
532 }544 }
533 545
534 // Protocol is a type that declares methods that can be implemented by any class.546 // Protocol is a type that declares methods that can be implemented by any class.
535-type Protocol [0]func()547+type Protocol uintptr
536 548
537 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.549 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
538 func GetProtocol(name string) *Protocol {550 func GetProtocol(name string) *Protocol {
539 return objc_getProtocol(name)551 return objc_getProtocol(name)
540 }552 }
541 553
542-// Equals return true if the two protocols are the same.
543-func (p *Protocol) Equals(p2 *Protocol) bool {
544- return protocol_isEqual(p, p2)
545-}
546-
547 // IMP is a function pointer that can be called by Objective-C code.554 // IMP is a function pointer that can be called by Objective-C code.
548 type IMP uintptr555 type IMP uintptr
549 556
550 // NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.557 // NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
558+// ID may instead be a pointer to a struct whose first field has type Class.
551 // It returns an IMP function pointer that can be called by Objective-C code.559 // It returns an IMP function pointer that can be called by Objective-C code.
552-// The function panics if an error occurs.
553 // The function pointer is never deallocated.560 // The function pointer is never deallocated.
554 func NewIMP(fn interface{}) IMP {561 func NewIMP(fn interface{}) IMP {
555 ty := reflect.TypeOf(fn)562 ty := reflect.TypeOf(fn)
@@ -561,9 +568,13 @@ func NewIMP(fn interface{}) IMP {
561 switch {568 switch {
562 case ty.NumIn() < 2:569 case ty.NumIn() < 2:
563 fallthrough570 fallthrough
564- case ty.In(0) != reflect.TypeOf(ID(0)):571+ case ty.In(0).Kind() != reflect.Uintptr && // checks if it's objc.ID
572+ // or that it's a pointer to a struct
573+ (ty.In(0).Kind() != reflect.Pointer || ty.In(0).Elem().Kind() != reflect.Struct ||
574+ // and that the structs first field is an objc.Class
575+ ty.In(0).Elem().Field(0).Type != reflect.TypeOf(Class(0))):
565 fallthrough576 fallthrough
566- case ty.In(1) != reflect.TypeOf(SEL(0)):577+ case ty.In(1).Kind() != reflect.Uintptr:
567 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())578 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
568 }579 }
569 return IMP(purego.NewCallback(fn))580 return IMP(purego.NewCallback(fn))
M objc/objc_runtime_darwin_test.go +19 -41
@@ -5,7 +5,6 @@ package objc_test
55
66 import (
77 "fmt"
8- "reflect"
98 "testing"
109
1110 "github.com/ebitengine/purego"
@@ -13,56 +12,35 @@ import (
1312 )
1413
1514 func ExampleAllocateClassPair() {
16- class := objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0)
15+ var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0)
1716 class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) {
1817 fmt.Println("Hello World!")
1918 }), "v@:")
2019 class.Register()
2120
22- fooObject := objc.ID(class).Send(objc.RegisterName("new"))
21+ var fooObject = objc.ID(class).Send(objc.RegisterName("new"))
2322 fooObject.Send(objc.RegisterName("run"))
2423 // Output: Hello World!
2524 }
2625
27-func ExampleRegisterClass() {
28- var (
29- sel_new = objc.RegisterName("new")
30- sel_init = objc.RegisterName("init")
31- sel_setBar = objc.RegisterName("setBar:")
32- sel_bar = objc.RegisterName("bar")
33-
34- BarInit = func(id objc.ID, cmd objc.SEL) objc.ID {
35- return id.SendSuper(cmd)
36- }
37- )
38-
39- class, err := objc.RegisterClass(
40- "BarObject",
41- objc.GetClass("NSObject"),
42- []*objc.Protocol{
43- objc.GetProtocol("NSDelegateWindow"),
44- },
45- []objc.FieldDef{
46- {
47- Name: "bar",
48- Type: reflect.TypeOf(int(0)),
49- Attribute: objc.ReadWrite,
50- },
51- },
52- []objc.MethodDef{
53- {
54- Cmd: sel_init,
55- Fn: BarInit,
56- },
57- },
58- )
59- if err != nil {
60- panic(err)
26+func ExampleClass_AddIvar() {
27+ type barObject struct {
28+ isa objc.Class
29+ bar int
6130 }
31+ var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "BarObject", 0)
32+ class.AddIvar("bar", int(0), "q")
33+ class.AddMethod(objc.RegisterName("bar"), objc.NewIMP(func(self *barObject, _cmd objc.SEL) int {
34+ return self.bar
35+ }), "q@:")
36+ class.AddMethod(objc.RegisterName("setBar:"), objc.NewIMP(func(self *barObject, _cmd objc.SEL, bar int) {
37+ self.bar = bar
38+ }), "v@:q")
39+ class.Register()
6240
63- object := objc.ID(class).Send(sel_new)
64- object.Send(sel_setBar, 123)
65- bar := int(object.Send(sel_bar))
41+ var object = objc.ID(class).Send(objc.RegisterName("new"))
42+ object.Send(objc.RegisterName("setBar:"), 123)
43+ var bar = int(object.Send(objc.RegisterName("bar")))
6644 fmt.Println(bar)
6745 // Output: 123
6846 }
@@ -107,7 +85,7 @@ func TestSend(t *testing.T) {
10785 // Initialize a NSNumber
10886 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)
10987 // Then get that number back using the generic Send function.
110- number := objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
88+ var number = objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
11189 if double != number {
11290 t.Failed()
11391 }
@@ -5,7 +5,6 @@ package objc_test
5 5
6 import (6 import (
7 "fmt"7 "fmt"
8- "reflect"
9 "testing"8 "testing"
10 9
11 "github.com/ebitengine/purego"10 "github.com/ebitengine/purego"
@@ -13,56 +12,35 @@ import (
13 )12 )
14 13
15 func ExampleAllocateClassPair() {14 func ExampleAllocateClassPair() {
16- class := objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0)15+ var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0)
17 class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) {16 class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) {
18 fmt.Println("Hello World!")17 fmt.Println("Hello World!")
19 }), "v@:")18 }), "v@:")
20 class.Register()19 class.Register()
21 20
22- fooObject := objc.ID(class).Send(objc.RegisterName("new"))21+ var fooObject = objc.ID(class).Send(objc.RegisterName("new"))
23 fooObject.Send(objc.RegisterName("run"))22 fooObject.Send(objc.RegisterName("run"))
24 // Output: Hello World!23 // Output: Hello World!
25 }24 }
26 25
27-func ExampleRegisterClass() {26+func ExampleClass_AddIvar() {
28- var (27+ type barObject struct {
29- sel_new = objc.RegisterName("new")28+ isa objc.Class
30- sel_init = objc.RegisterName("init")29+ bar int
31- sel_setBar = objc.RegisterName("setBar:")
32- sel_bar = objc.RegisterName("bar")
33-
34- BarInit = func(id objc.ID, cmd objc.SEL) objc.ID {
35- return id.SendSuper(cmd)
36- }
37- )
38-
39- class, err := objc.RegisterClass(
40- "BarObject",
41- objc.GetClass("NSObject"),
42- []*objc.Protocol{
43- objc.GetProtocol("NSDelegateWindow"),
44- },
45- []objc.FieldDef{
46- {
47- Name: "bar",
48- Type: reflect.TypeOf(int(0)),
49- Attribute: objc.ReadWrite,
50- },
51- },
52- []objc.MethodDef{
53- {
54- Cmd: sel_init,
55- Fn: BarInit,
56- },
57- },
58- )
59- if err != nil {
60- panic(err)
61 }30 }
31+ var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "BarObject", 0)
32+ class.AddIvar("bar", int(0), "q")
33+ class.AddMethod(objc.RegisterName("bar"), objc.NewIMP(func(self *barObject, _cmd objc.SEL) int {
34+ return self.bar
35+ }), "q@:")
36+ class.AddMethod(objc.RegisterName("setBar:"), objc.NewIMP(func(self *barObject, _cmd objc.SEL, bar int) {
37+ self.bar = bar
38+ }), "v@:q")
39+ class.Register()
62 40
63- object := objc.ID(class).Send(sel_new)41+ var object = objc.ID(class).Send(objc.RegisterName("new"))
64- object.Send(sel_setBar, 123)42+ object.Send(objc.RegisterName("setBar:"), 123)
65- bar := int(object.Send(sel_bar))43+ var bar = int(object.Send(objc.RegisterName("bar")))
66 fmt.Println(bar)44 fmt.Println(bar)
67 // Output: 12345 // Output: 123
68 }46 }
@@ -107,7 +85,7 @@ func TestSend(t *testing.T) {
107 // Initialize a NSNumber85 // Initialize a NSNumber
108 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)86 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)
109 // Then get that number back using the generic Send function.87 // Then get that number back using the generic Send function.
110- number := objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))88+ var number = objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
111 if double != number {89 if double != number {
112 t.Failed()90 t.Failed()
113 }91 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close