detached 469fcb9

Implement safe ObjC classes (#157)

Closes #143
commit
469fcb91e836003463c0002935c4a383441f3283
author
TotallyGamerJet · · 2023-09-07 13:48 UTC
committer
GitHub · · 2023-09-07 13:48 UTC
parent
af306f3
signature
GPG Unverified4AEE18F83AFDEB23 The key is unknown
3 files · +241 -231
M examples/objc/main_darwin.go +31 -32
@@ -5,7 +5,7 @@ package main
55
66 import (
77 "fmt"
8- "unsafe"
8+ "reflect"
99
1010 "github.com/ebitengine/purego/objc"
1111 )
@@ -17,45 +17,44 @@ var (
1717 sel_bar = objc.RegisterName("bar")
1818 )
1919
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- }
20+func BarInit(id objc.ID, cmd objc.SEL) objc.ID {
21+ return id.SendSuper(cmd)
4822 }
4923
5024 func main() {
51- class, err := objc.RegisterClass(&barObject{})
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+ )
5251 if err != nil {
5352 panic(err)
5453 }
5554
56- var object = objc.ID(class).Send(sel_new)
55+ object := objc.ID(class).Send(sel_new)
5756 object.Send(sel_setBar, 123)
58- var bar = int(object.Send(sel_bar))
57+ bar := int(object.Send(sel_bar))
5958 fmt.Println(bar)
6059 // Output: 123
6160 }
@@ -5,7 +5,7 @@ package main
5 5
6 import (6 import (
7 "fmt"7 "fmt"
8- "unsafe"8+ "reflect"
9 9
10 "github.com/ebitengine/purego/objc"10 "github.com/ebitengine/purego/objc"
11 )11 )
@@ -17,45 +17,44 @@ var (
17 sel_bar = objc.RegisterName("bar")17 sel_bar = objc.RegisterName("bar")
18 )18 )
19 19
20-type barObject struct {20+func BarInit(id objc.ID, cmd objc.SEL) objc.ID {
21- isa objc.Class `objc:"BarObject : NSObject <NSDelegateWindow>"`21+ return id.SendSuper(cmd)
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- }
48 }22 }
49 23
50 func main() {24 func main() {
51- class, err := objc.RegisterClass(&barObject{})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+ )
52 if err != nil {51 if err != nil {
53 panic(err)52 panic(err)
54 }53 }
55 54
56- var object = objc.ID(class).Send(sel_new)55+ object := objc.ID(class).Send(sel_new)
57 object.Send(sel_setBar, 123)56 object.Send(sel_setBar, 123)
58- var bar = int(object.Send(sel_bar))57+ bar := int(object.Send(sel_bar))
59 fmt.Println(bar)58 fmt.Println(bar)
60 // Output: 12359 // Output: 123
61 }60 }
M objc/objc_runtime_darwin.go +169 -180
@@ -10,13 +10,15 @@ import (
1010 "fmt"
1111 "math"
1212 "reflect"
13+ "regexp"
14+ "unicode"
15+ "unsafe"
1316
1417 "github.com/ebitengine/purego"
1518 )
1619
17-//TODO: support try/catch?
18-//https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
19-
20+// TODO: support try/catch?
21+// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
2022 var (
2123 objc_msgSend_fn uintptr
2224 objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID
@@ -35,6 +37,10 @@ var (
3537 class_addProtocol func(class Class, protocol *Protocol) bool
3638 ivar_getOffset func(ivar Ivar) uintptr
3739 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
3844 )
3945
4046 func init() {
@@ -65,6 +71,10 @@ func init() {
6571 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
6672 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
6773 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")
6878 }
6979
7080 // ID is an opaque pointer to some Objective-C object
@@ -82,6 +92,16 @@ func (id ID) Send(sel SEL, args ...interface{}) ID {
8292 return objc_msgSend(id, sel, args...)
8393 }
8494
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+
85105 // Send is a convenience method for sending messages to objects that can return any type.
86106 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
87107 // It is best to cache the result of RegisterName.
@@ -103,7 +123,7 @@ type objc_super struct {
103123 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
104124 // of RegisterName.
105125 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
106- var super = &objc_super{
126+ super := &objc_super{
107127 receiver: id,
108128 superClass: id.Class(),
109129 }
@@ -114,7 +134,7 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
114134 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
115135 // It is best to cache the result of RegisterName.
116136 func SendSuper[T any](id ID, sel SEL, args ...any) T {
117- var super = &objc_super{
137+ super := &objc_super{
118138 receiver: id,
119139 superClass: id.Class(),
120140 }
@@ -148,203 +168,171 @@ func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
148168 return objc_allocateClassPair(super, name, extraBytes)
149169 }
150170
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
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
157175 }
158176
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, ...>""`)
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
161192
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")
193+const (
194+ ReadOnly IvarAttrib = 1 << iota
195+ ReadWrite
196+)
164197
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:]
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+}
214209
215- skipSpace()
210+// ivarRegex checks to make sure the Ivar is correctly formatted
211+var ivarRegex = regexp.MustCompile("[a-z][a-zA-Z0-9]*")
216212
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)
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)
281218 if class == 0 {
282- return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])
219+ return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
283220 }
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- }
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))
290225 }
291226 }
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()
227+ // Add exported methods based on the selectors returned from ClassDef(string) SEL
228+ for idx, def := range methods {
305229 imp, err := func() (imp IMP, err error) {
306230 defer func() {
307231 if r := recover(); r != nil {
308232 err = fmt.Errorf("objc: failed to create IMP: %s", r)
309233 }
310234 }()
311- return NewIMP(fn), nil
235+ return NewIMP(def.Fn), nil
312236 }()
313237 if err != nil {
314- return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
238+ return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
315239 }
316- encoding, err := encodeFunc(fn)
240+ encoding, err := encodeFunc(def.Fn)
317241 if err != nil {
318- return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
242+ return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
319243 }
320- if !class.AddMethod(sel, imp, encoding) {
321- return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)
244+ if !class.AddMethod(def.Cmd, imp, encoding) {
245+ return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
322246 }
323247 }
324248 // Add Ivars
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
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)
330252 }
331- size := f.Type.Size()
332- alignment := uint8(math.Log2(float64(f.Type.Align())))
333- enc, err := encodeType(f.Type, false)
253+ size := ivar.Type.Size()
254+ alignment := uint8(math.Log2(float64(ivar.Type.Align())))
255+ enc, err := encodeType(ivar.Type, false)
334256 if err != nil {
335- return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)
257+ return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
336258 }
337- if !class_addIvar(class, f.Name, size, alignment, enc) {
338- return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)
259+ if !class_addIvar(class, ivar.Name, size, alignment, enc) {
260+ return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
339261 }
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)
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)
342333 }
343334 }
344335 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- }
348336 return class, nil
349337 }
350338
@@ -420,7 +408,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
420408 if insidePtr {
421409 return encStructBegin + typ.Name() + encStructEnd, nil
422410 }
423- var encoding = encStructBegin
411+ encoding := encStructBegin
424412 encoding += typ.Name()
425413 encoding += "="
426414 for i := 0; i < typ.NumField(); i++ {
@@ -544,19 +532,24 @@ func (i Ivar) Offset() uintptr {
544532 }
545533
546534 // Protocol is a type that declares methods that can be implemented by any class.
547-type Protocol uintptr
535+type Protocol [0]func()
548536
549537 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
550538 func GetProtocol(name string) *Protocol {
551539 return objc_getProtocol(name)
552540 }
553541
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+
554547 // IMP is a function pointer that can be called by Objective-C code.
555548 type IMP uintptr
556549
557550 // 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.
559551 // It returns an IMP function pointer that can be called by Objective-C code.
552+// The function panics if an error occurs.
560553 // The function pointer is never deallocated.
561554 func NewIMP(fn interface{}) IMP {
562555 ty := reflect.TypeOf(fn)
@@ -568,13 +561,9 @@ func NewIMP(fn interface{}) IMP {
568561 switch {
569562 case ty.NumIn() < 2:
570563 fallthrough
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))):
564+ case ty.In(0) != reflect.TypeOf(ID(0)):
576565 fallthrough
577- case ty.In(1).Kind() != reflect.Uintptr:
566+ case ty.In(1) != reflect.TypeOf(SEL(0)):
578567 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
579568 }
580569 return IMP(purego.NewCallback(fn))
@@ -10,13 +10,15 @@ import (
10 "fmt"10 "fmt"
11 "math"11 "math"
12 "reflect"12 "reflect"
13+ "regexp"
14+ "unicode"
15+ "unsafe"
13 16
14 "github.com/ebitengine/purego"17 "github.com/ebitengine/purego"
15 )18 )
16 19
17-//TODO: support try/catch?20+// TODO: support try/catch?
18-//https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime21+// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
19-
20 var (22 var (
21 objc_msgSend_fn uintptr23 objc_msgSend_fn uintptr
22 objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID24 objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID
@@ -35,6 +37,10 @@ var (
35 class_addProtocol func(class Class, protocol *Protocol) bool37 class_addProtocol func(class Class, protocol *Protocol) bool
36 ivar_getOffset func(ivar Ivar) uintptr38 ivar_getOffset func(ivar Ivar) uintptr
37 object_getClass func(obj ID) Class39 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
38 )44 )
39 45
40 func init() {46 func init() {
@@ -65,6 +71,10 @@ func init() {
65 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")71 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
66 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")72 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
67 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")73 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")
68 }78 }
69 79
70 // ID is an opaque pointer to some Objective-C object80 // ID is an opaque pointer to some Objective-C object
@@ -82,6 +92,16 @@ func (id ID) Send(sel SEL, args ...interface{}) ID {
82 return objc_msgSend(id, sel, args...)92 return objc_msgSend(id, sel, args...)
83 }93 }
84 94
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+
85 // Send is a convenience method for sending messages to objects that can return any type.105 // Send is a convenience method for sending messages to objects that can return any type.
86 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.106 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
87 // It is best to cache the result of RegisterName.107 // It is best to cache the result of RegisterName.
@@ -103,7 +123,7 @@ type objc_super struct {
103 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result123 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
104 // of RegisterName.124 // of RegisterName.
105 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {125 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
106- var super = &objc_super{126+ super := &objc_super{
107 receiver: id,127 receiver: id,
108 superClass: id.Class(),128 superClass: id.Class(),
109 }129 }
@@ -114,7 +134,7 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
114 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.134 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
115 // It is best to cache the result of RegisterName.135 // It is best to cache the result of RegisterName.
116 func SendSuper[T any](id ID, sel SEL, args ...any) T {136 func SendSuper[T any](id ID, sel SEL, args ...any) T {
117- var super = &objc_super{137+ super := &objc_super{
118 receiver: id,138 receiver: id,
119 superClass: id.Class(),139 superClass: id.Class(),
120 }140 }
@@ -148,203 +168,171 @@ func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
148 return objc_allocateClassPair(super, name, extraBytes)168 return objc_allocateClassPair(super, name, extraBytes)
149 }169 }
150 170
151-// Selector is an interface that takes a Go method name171+// MethodDef represents the Go function and the selector that ObjC uses to access that function.
152-// and returns the selector equivalent name.172+type MethodDef struct {
153-// If it returns a nil SEL then that method173+ Cmd SEL
154-// is not added to the class object.174+ Fn any
155-type Selector interface {
156- Selector(string) SEL
157 }175 }
158 176
159-// TagFormatError occurs when the parser fails to parse the objc tag in a Selector object177+// IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically
160-var TagFormatError = errors.New(`objc tag doesn't match "ClassName : SuperClassName <Protocol, ...>""`)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
161 192
162-// MismatchError occurs when the Go struct definition doesn't match that of Objective-C193+const (
163-var MismatchError = errors.New("go struct doesn't match objective-c struct")194+ ReadOnly IvarAttrib = 1 << iota
195+ ReadWrite
196+)
164 197
165-// RegisterClass takes a pointer to a struct that implements the Selector interface.198+// FieldDef is a definition of a field to add to an Objective-C class.
166-// It will register the structs fields and pointer receiver methods in the Objective-C199+// The name of the field is what will be used to access it through the Ivar. If the type is bool
167-// runtime using the SEL returned from Selector. Any errors that occur trying to add200+// the name cannot start with `is` since a getter will be generated with the name `isBoolName`.
168-// a Method or Ivar is returned as an error. Such errors may occur in parsing or because201+// The name also cannot contain any spaces.
169-// the size of the struct does not match the size in Objective-C. If no errors occur202+// The type is the Go equivalent type of the Ivar.
170-// then the returned Class has been registered successfully.203+// Attribute determines if a getter and or setter method is generated for this field.
171-//204+type FieldDef struct {
172-// The struct's first field must be of type Class and have a tag that matches the format205+ Name string
173-// `objc:"ClassName : SuperClassName <Protocol, ...>`. This tag is equal to how the class206+ Type reflect.Type
174-// would be defined in Objective-C.207+ Attribute IvarAttrib
175-func RegisterClass(object Selector) (Class, error) {208+}
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:]
214 209
215- skipSpace()210+// ivarRegex checks to make sure the Ivar is correctly formatted
211+var ivarRegex = regexp.MustCompile("[a-z][a-zA-Z0-9]*")
216 212
217- // check for ':'213+// RegisterClass takes the name of the class to create, the superclass, a list of protocols this class
218- if len(tag) > 0 && tag[0] != ':' {214+// implements, a list of fields this class has and a list of methods. It returns the created class or an error
219- return 0, fmt.Errorf("objc: missing ':': %w", TagFormatError)215+// describing what went wrong.
220- }216+func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {
221- tag = tag[1:] // skip ':'217+ class := objc_allocateClassPair(superClass, name, 0)
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)
281 if class == 0 {218 if class == 0 {
282- return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])219+ return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
283 }220 }
284- if len(split) > 2 {221+ // Add Protocols
285- // Add Protocols222+ for _, p := range protocols {
286- for _, n := range split[2:] {223+ if !class.AddProtocol(p) {
287- if !class.AddProtocol(GetProtocol(n)) {224+ return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p))
288- return 0, fmt.Errorf("objc: couldn't add Protocol %s", n)
289- }
290 }225 }
291 }226 }
292- // Add exported methods based on the selectors returned from Selector(string) SEL227+ // Add exported methods based on the selectors returned from ClassDef(string) SEL
293- for i := 0; i < ptr.NumMethod(); i++ {228+ for idx, def := range methods {
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()
305 imp, err := func() (imp IMP, err error) {229 imp, err := func() (imp IMP, err error) {
306 defer func() {230 defer func() {
307 if r := recover(); r != nil {231 if r := recover(); r != nil {
308 err = fmt.Errorf("objc: failed to create IMP: %s", r)232 err = fmt.Errorf("objc: failed to create IMP: %s", r)
309 }233 }
310 }()234 }()
311- return NewIMP(fn), nil235+ return NewIMP(def.Fn), nil
312 }()236 }()
313 if err != nil {237 if err != nil {
314- return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)238+ return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
315 }239 }
316- encoding, err := encodeFunc(fn)240+ encoding, err := encodeFunc(def.Fn)
317 if err != nil {241 if err != nil {
318- return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)242+ return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
319 }243 }
320- if !class.AddMethod(sel, imp, encoding) {244+ if !class.AddMethod(def.Cmd, imp, encoding) {
321- return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)245+ return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
322 }246 }
323 }247 }
324 // Add Ivars248 // Add Ivars
325- // Start at 1 because we skip the class object which is first249+ for _, ivar := range ivars {
326- for i := 1; i < strct.NumField(); i++ {250+ if !ivarRegex.MatchString(ivar.Name) {
327- f := strct.Field(i)251+ return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name)
328- if f.Name == "_" {
329- continue
330 }252 }
331- size := f.Type.Size()253+ size := ivar.Type.Size()
332- alignment := uint8(math.Log2(float64(f.Type.Align())))254+ alignment := uint8(math.Log2(float64(ivar.Type.Align())))
333- enc, err := encodeType(f.Type, false)255+ enc, err := encodeType(ivar.Type, false)
334 if err != nil {256 if err != nil {
335- return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)257+ return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
336 }258 }
337- if !class_addIvar(class, f.Name, size, alignment, enc) {259+ if !class_addIvar(class, ivar.Name, size, alignment, enc) {
338- return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)260+ return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
339 }261 }
340- if offset := class.InstanceVariable(f.Name).Offset(); offset != f.Offset {262+ switch ivar.Attribute {
341- return 0, fmt.Errorf("objc: couldn't add Ivar %s because offset (%d != %d)", f.Name, offset, f.Offset)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)
342 }333 }
343 }334 }
344 objc_registerClassPair(class)335 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- }
348 return class, nil336 return class, nil
349 }337 }
350 338
@@ -420,7 +408,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
420 if insidePtr {408 if insidePtr {
421 return encStructBegin + typ.Name() + encStructEnd, nil409 return encStructBegin + typ.Name() + encStructEnd, nil
422 }410 }
423- var encoding = encStructBegin411+ encoding := encStructBegin
424 encoding += typ.Name()412 encoding += typ.Name()
425 encoding += "="413 encoding += "="
426 for i := 0; i < typ.NumField(); i++ {414 for i := 0; i < typ.NumField(); i++ {
@@ -544,19 +532,24 @@ func (i Ivar) Offset() uintptr {
544 }532 }
545 533
546 // Protocol is a type that declares methods that can be implemented by any class.534 // Protocol is a type that declares methods that can be implemented by any class.
547-type Protocol uintptr535+type Protocol [0]func()
548 536
549 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.537 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
550 func GetProtocol(name string) *Protocol {538 func GetProtocol(name string) *Protocol {
551 return objc_getProtocol(name)539 return objc_getProtocol(name)
552 }540 }
553 541
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+
554 // IMP is a function pointer that can be called by Objective-C code.547 // IMP is a function pointer that can be called by Objective-C code.
555 type IMP uintptr548 type IMP uintptr
556 549
557 // NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.550 // 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.
559 // It returns an IMP function pointer that can be called by Objective-C code.551 // It returns an IMP function pointer that can be called by Objective-C code.
552+// The function panics if an error occurs.
560 // The function pointer is never deallocated.553 // The function pointer is never deallocated.
561 func NewIMP(fn interface{}) IMP {554 func NewIMP(fn interface{}) IMP {
562 ty := reflect.TypeOf(fn)555 ty := reflect.TypeOf(fn)
@@ -568,13 +561,9 @@ func NewIMP(fn interface{}) IMP {
568 switch {561 switch {
569 case ty.NumIn() < 2:562 case ty.NumIn() < 2:
570 fallthrough563 fallthrough
571- case ty.In(0).Kind() != reflect.Uintptr && // checks if it's objc.ID564+ case ty.In(0) != reflect.TypeOf(ID(0)):
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))):
576 fallthrough565 fallthrough
577- case ty.In(1).Kind() != reflect.Uintptr:566+ case ty.In(1) != reflect.TypeOf(SEL(0)):
578 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())567 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
579 }568 }
580 return IMP(purego.NewCallback(fn))569 return IMP(purego.NewCallback(fn))
M objc/objc_runtime_darwin_test.go +41 -19
@@ -5,6 +5,7 @@ package objc_test
55
66 import (
77 "fmt"
8+ "reflect"
89 "testing"
910
1011 "github.com/ebitengine/purego"
@@ -12,35 +13,56 @@ import (
1213 )
1314
1415 func ExampleAllocateClassPair() {
15- var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0)
16+ class := objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0)
1617 class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) {
1718 fmt.Println("Hello World!")
1819 }), "v@:")
1920 class.Register()
2021
21- var fooObject = objc.ID(class).Send(objc.RegisterName("new"))
22+ fooObject := objc.ID(class).Send(objc.RegisterName("new"))
2223 fooObject.Send(objc.RegisterName("run"))
2324 // Output: Hello World!
2425 }
2526
26-func ExampleClass_AddIvar() {
27- type barObject struct {
28- isa objc.Class
29- bar int
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)
3061 }
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()
4062
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")))
63+ object := objc.ID(class).Send(sel_new)
64+ object.Send(sel_setBar, 123)
65+ bar := int(object.Send(sel_bar))
4466 fmt.Println(bar)
4567 // Output: 123
4668 }
@@ -85,7 +107,7 @@ func TestSend(t *testing.T) {
85107 // Initialize a NSNumber
86108 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)
87109 // Then get that number back using the generic Send function.
88- var number = objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
110+ number := objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
89111 if double != number {
90112 t.Failed()
91113 }
@@ -5,6 +5,7 @@ package objc_test
5 5
6 import (6 import (
7 "fmt"7 "fmt"
8+ "reflect"
8 "testing"9 "testing"
9 10
10 "github.com/ebitengine/purego"11 "github.com/ebitengine/purego"
@@ -12,35 +13,56 @@ import (
12 )13 )
13 14
14 func ExampleAllocateClassPair() {15 func ExampleAllocateClassPair() {
15- var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0)16+ class := objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0)
16 class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) {17 class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) {
17 fmt.Println("Hello World!")18 fmt.Println("Hello World!")
18 }), "v@:")19 }), "v@:")
19 class.Register()20 class.Register()
20 21
21- var fooObject = objc.ID(class).Send(objc.RegisterName("new"))22+ fooObject := objc.ID(class).Send(objc.RegisterName("new"))
22 fooObject.Send(objc.RegisterName("run"))23 fooObject.Send(objc.RegisterName("run"))
23 // Output: Hello World!24 // Output: Hello World!
24 }25 }
25 26
26-func ExampleClass_AddIvar() {27+func ExampleRegisterClass() {
27- type barObject struct {28+ var (
28- isa objc.Class29+ sel_new = objc.RegisterName("new")
29- bar int30+ 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)
30 }61 }
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()
40 62
41- var object = objc.ID(class).Send(objc.RegisterName("new"))63+ object := objc.ID(class).Send(sel_new)
42- object.Send(objc.RegisterName("setBar:"), 123)64+ object.Send(sel_setBar, 123)
43- var bar = int(object.Send(objc.RegisterName("bar")))65+ bar := int(object.Send(sel_bar))
44 fmt.Println(bar)66 fmt.Println(bar)
45 // Output: 12367 // Output: 123
46 }68 }
@@ -85,7 +107,7 @@ func TestSend(t *testing.T) {
85 // Initialize a NSNumber107 // Initialize a NSNumber
86 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)108 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)
87 // Then get that number back using the generic Send function.109 // Then get that number back using the generic Send function.
88- var number = objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))110+ number := objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
89 if double != number {111 if double != number {
90 t.Failed()112 t.Failed()
91 }113 }

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