detached 6319229

objc: add safe Objc Classes (#160)

Closes #143
commit
63192295690dab022ec6fab0f2e0a80deae002c4
author
TotallyGamerJet · · 2023-09-09 15:48 UTC
committer
GitHub · · 2023-09-09 15:48 UTC
parent
0f25ec6
signature
GPG Unverified4AEE18F83AFDEB23 The key is unknown
refs
tag: v0.5.0-alpha.3
4 files · +246 -233
M examples/objc/main_darwin.go +30 -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,43 @@ 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+ class, err := objc.RegisterClass(
31+ "BarObject",
32+ objc.GetClass("NSObject"),
33+ []*objc.Protocol{
34+ objc.GetProtocol("NSDelegateWindow"),
35+ },
36+ []objc.FieldDef{
37+ {
38+ Name: "bar",
39+ Type: reflect.TypeOf(int(0)),
40+ Attribute: objc.ReadWrite,
41+ },
42+ },
43+ []objc.MethodDef{
44+ {
45+ Cmd: sel_init,
46+ Fn: BarInit,
47+ },
48+ },
49+ )
5250 if err != nil {
5351 panic(err)
5452 }
5553
56- var object = objc.ID(class).Send(sel_new)
54+ object := objc.ID(class).Send(sel_new)
5755 object.Send(sel_setBar, 123)
58- var bar = int(object.Send(sel_bar))
56+ bar := int(object.Send(sel_bar))
5957 fmt.Println(bar)
6058 // Output: 123
6159 }
@@ -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,43 @@ 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+ class, err := objc.RegisterClass(
31+ "BarObject",
32+ objc.GetClass("NSObject"),
33+ []*objc.Protocol{
34+ objc.GetProtocol("NSDelegateWindow"),
35+ },
36+ []objc.FieldDef{
37+ {
38+ Name: "bar",
39+ Type: reflect.TypeOf(int(0)),
40+ Attribute: objc.ReadWrite,
41+ },
42+ },
43+ []objc.MethodDef{
44+ {
45+ Cmd: sel_init,
46+ Fn: BarInit,
47+ },
48+ },
49+ )
52 if err != nil {50 if err != nil {
53 panic(err)51 panic(err)
54 }52 }
55 53
56- var object = objc.ID(class).Send(sel_new)54+ object := objc.ID(class).Send(sel_new)
57 object.Send(sel_setBar, 123)55 object.Send(sel_setBar, 123)
58- var bar = int(object.Send(sel_bar))56+ bar := int(object.Send(sel_bar))
59 fmt.Println(bar)57 fmt.Println(bar)
60 // Output: 12358 // Output: 123
61 }59 }
M internal/strings/strings.go +2 -2
@@ -17,12 +17,12 @@ func CString(name string) *byte {
1717 if hasSuffix(name, "\x00") {
1818 return &(*(*[]byte)(unsafe.Pointer(&name)))[0]
1919 }
20- var b = make([]byte, len(name)+1)
20+ b := make([]byte, len(name)+1)
2121 copy(b, name)
2222 return &b[0]
2323 }
2424
25-// GoString copies a char* to a Go string.
25+// GoString copies a null-terminated char* to a Go string.
2626 func GoString(c uintptr) string {
2727 // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer
2828 ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c))
@@ -17,12 +17,12 @@ func CString(name string) *byte {
17 if hasSuffix(name, "\x00") {17 if hasSuffix(name, "\x00") {
18 return &(*(*[]byte)(unsafe.Pointer(&name)))[0]18 return &(*(*[]byte)(unsafe.Pointer(&name)))[0]
19 }19 }
20- var b = make([]byte, len(name)+1)20+ b := make([]byte, len(name)+1)
21 copy(b, name)21 copy(b, name)
22 return &b[0]22 return &b[0]
23 }23 }
24 24
25-// GoString copies a char* to a Go string.25+// GoString copies a null-terminated char* to a Go string.
26 func GoString(c uintptr) string {26 func GoString(c uintptr) string {
27 // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer27 // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer
28 ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c))28 ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c))
M objc/objc_runtime_darwin.go +168 -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
@@ -34,7 +36,12 @@ var (
3436 class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool
3537 class_addProtocol func(class Class, protocol *Protocol) bool
3638 ivar_getOffset func(ivar Ivar) uintptr
39+ ivar_getName func(ivar Ivar) string
3740 object_getClass func(obj ID) Class
41+ object_getIvar func(obj ID, ivar Ivar) ID
42+ object_setIvar func(obj ID, ivar Ivar, value ID)
43+ protocol_getName func(protocol *Protocol) string
44+ protocol_isEqual func(p *Protocol, p2 *Protocol) bool
3845 )
3946
4047 func init() {
@@ -65,6 +72,11 @@ func init() {
6572 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
6673 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
6774 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
75+ purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName")
76+ purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
77+ purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
78+ purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
79+ purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
6880 }
6981
7082 // ID is an opaque pointer to some Objective-C object
@@ -82,6 +94,16 @@ func (id ID) Send(sel SEL, args ...interface{}) ID {
8294 return objc_msgSend(id, sel, args...)
8395 }
8496
97+// GetIvar reads the value of an instance variable in an object.
98+func (id ID) GetIvar(ivar Ivar) ID {
99+ return object_getIvar(id, ivar)
100+}
101+
102+// SetIvar sets the value of an instance variable in an object.
103+func (id ID) SetIvar(ivar Ivar, value ID) {
104+ object_setIvar(id, ivar, value)
105+}
106+
85107 // Send is a convenience method for sending messages to objects that can return any type.
86108 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
87109 // It is best to cache the result of RegisterName.
@@ -103,7 +125,7 @@ type objc_super struct {
103125 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
104126 // of RegisterName.
105127 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
106- var super = &objc_super{
128+ super := &objc_super{
107129 receiver: id,
108130 superClass: id.Class(),
109131 }
@@ -114,7 +136,7 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
114136 // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
115137 // It is best to cache the result of RegisterName.
116138 func SendSuper[T any](id ID, sel SEL, args ...any) T {
117- var super = &objc_super{
139+ super := &objc_super{
118140 receiver: id,
119141 superClass: id.Class(),
120142 }
@@ -148,203 +170,164 @@ func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
148170 return objc_allocateClassPair(super, name, extraBytes)
149171 }
150172
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
173+// MethodDef represents the Go function and the selector that ObjC uses to access that function.
174+type MethodDef struct {
175+ Cmd SEL
176+ Fn any
157177 }
158178
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, ...>""`)
179+// IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically
180+// generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes.
181+// The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib.
182+//
183+// Take for example this Objective-C code:
184+//
185+// @property (readwrite) float value;
186+//
187+// In Go, the functions can be accessed as followed:
188+//
189+// var value = purego.Send[float32](id, purego.RegisterName("value"))
190+// id.Send(purego.RegisterName("setValue:"), 3.46)
191+//
192+// [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html
193+type IvarAttrib int
161194
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")
195+const (
196+ ReadOnly IvarAttrib = 1 << iota
197+ ReadWrite
198+)
164199
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:]
200+// FieldDef is a definition of a field to add to an Objective-C class.
201+// The name of the field is what will be used to access it through the Ivar. If the type is bool
202+// the name cannot start with `is` since a getter will be generated with the name `isBoolName`.
203+// The name also cannot contain any spaces.
204+// The type is the Go equivalent type of the Ivar.
205+// Attribute determines if a getter and or setter method is generated for this field.
206+type FieldDef struct {
207+ Name string
208+ Type reflect.Type
209+ Attribute IvarAttrib
210+}
214211
215- skipSpace()
212+// ivarRegex checks to make sure the Ivar is correctly formatted
213+var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*")
216214
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)
215+// RegisterClass takes the name of the class to create, the superclass, a list of protocols this class
216+// implements, a list of fields this class has and a list of methods. It returns the created class or an error
217+// describing what went wrong.
218+func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {
219+ class := objc_allocateClassPair(superClass, name, 0)
281220 if class == 0 {
282- return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])
221+ return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
283222 }
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- }
223+ // Add Protocols
224+ for _, p := range protocols {
225+ if !class.AddProtocol(p) {
226+ return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p))
290227 }
291228 }
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()
229+ // Add exported methods based on the selectors returned from ClassDef(string) SEL
230+ for idx, def := range methods {
305231 imp, err := func() (imp IMP, err error) {
306232 defer func() {
307233 if r := recover(); r != nil {
308234 err = fmt.Errorf("objc: failed to create IMP: %s", r)
309235 }
310236 }()
311- return NewIMP(fn), nil
237+ return NewIMP(def.Fn), nil
312238 }()
313239 if err != nil {
314- return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
240+ return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
315241 }
316- encoding, err := encodeFunc(fn)
242+ encoding, err := encodeFunc(def.Fn)
317243 if err != nil {
318- return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
244+ return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
319245 }
320- if !class.AddMethod(sel, imp, encoding) {
321- return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)
246+ if !class.AddMethod(def.Cmd, imp, encoding) {
247+ return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
322248 }
323249 }
324250 // 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
251+ for _, instVar := range ivars {
252+ ivar := instVar
253+ if !ivarRegex.MatchString(ivar.Name) {
254+ return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name)
330255 }
331- size := f.Type.Size()
332- alignment := uint8(math.Log2(float64(f.Type.Align())))
333- enc, err := encodeType(f.Type, false)
256+ size := ivar.Type.Size()
257+ alignment := uint8(math.Log2(float64(ivar.Type.Align())))
258+ enc, err := encodeType(ivar.Type, false)
334259 if err != nil {
335- return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)
260+ return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
336261 }
337- if !class_addIvar(class, f.Name, size, alignment, enc) {
338- return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)
262+ if !class_addIvar(class, ivar.Name, size, alignment, enc) {
263+ return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
339264 }
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)
265+ offset := class.InstanceVariable(ivar.Name).Offset()
266+ switch ivar.Attribute {
267+ case ReadWrite:
268+ ty := reflect.FuncOf(
269+ []reflect.Type{
270+ reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type,
271+ },
272+ nil, false,
273+ )
274+ var encoding string
275+ if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
276+ return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
277+ }
278+ val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
279+ // on entry the first and second arguments are ID and SEL followed by the value
280+ if len(args) != 3 {
281+ panic(fmt.Sprintf("objc: incorrect number of args. expected 3 got %d", len(args)))
282+ }
283+ // The following reflect code does the equivalent of this:
284+ //
285+ // ((*struct {
286+ // Padding [offset]byte
287+ // Value int
288+ // })(unsafe.Pointer(args[0].Interface().(ID)))).v = 123
289+ //
290+ // However, since the type of the variable is unknown reflection is used to actually assign the value
291+ id := args[0].Interface().(ID)
292+ ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
293+ reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem().Set(args[2])
294+ return nil
295+ }).Interface()
296+ // this code only works for ascii but that shouldn't be a problem
297+ selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00"
298+ class.AddMethod(RegisterName(selector), NewIMP(val), encoding)
299+ fallthrough // also implement the read method
300+ case ReadOnly:
301+ ty := reflect.FuncOf(
302+ []reflect.Type{
303+ reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)),
304+ },
305+ []reflect.Type{ivar.Type}, false,
306+ )
307+ var encoding string
308+ if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
309+ return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
310+ }
311+ val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
312+ // on entry the first and second arguments are ID and SEL
313+ if len(args) != 2 {
314+ panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args)))
315+ }
316+ id := args[0].Interface().(ID)
317+ ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
318+ // the variable is located at an offset from the id
319+ return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem()}
320+ }).Interface()
321+ if ivar.Type.Kind() == reflect.Bool {
322+ // this code only works for ascii but that shouldn't be a problem
323+ ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:]
324+ }
325+ class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding)
326+ default:
327+ return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute)
342328 }
343329 }
344330 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- }
348331 return class, nil
349332 }
350333
@@ -420,7 +403,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
420403 if insidePtr {
421404 return encStructBegin + typ.Name() + encStructEnd, nil
422405 }
423- var encoding = encStructBegin
406+ encoding := encStructBegin
424407 encoding += typ.Name()
425408 encoding += "="
426409 for i := 0; i < typ.NumField(); i++ {
@@ -543,20 +526,29 @@ func (i Ivar) Offset() uintptr {
543526 return ivar_getOffset(i)
544527 }
545528
529+func (i Ivar) Name() string {
530+ return ivar_getName(i)
531+}
532+
546533 // Protocol is a type that declares methods that can be implemented by any class.
547-type Protocol uintptr
534+type Protocol [0]func()
548535
549536 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
550537 func GetProtocol(name string) *Protocol {
551538 return objc_getProtocol(name)
552539 }
553540
541+// Equals return true if the two protocols are the same.
542+func (p *Protocol) Equals(p2 *Protocol) bool {
543+ return protocol_isEqual(p, p2)
544+}
545+
554546 // IMP is a function pointer that can be called by Objective-C code.
555547 type IMP uintptr
556548
557549 // 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.
559550 // It returns an IMP function pointer that can be called by Objective-C code.
551+// The function panics if an error occurs.
560552 // The function pointer is never deallocated.
561553 func NewIMP(fn interface{}) IMP {
562554 ty := reflect.TypeOf(fn)
@@ -568,13 +560,9 @@ func NewIMP(fn interface{}) IMP {
568560 switch {
569561 case ty.NumIn() < 2:
570562 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))):
563+ case ty.In(0) != reflect.TypeOf(ID(0)):
576564 fallthrough
577- case ty.In(1).Kind() != reflect.Uintptr:
565+ case ty.In(1) != reflect.TypeOf(SEL(0)):
578566 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
579567 }
580568 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
@@ -34,7 +36,12 @@ var (
34 class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool36 class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool
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
39+ ivar_getName func(ivar Ivar) string
37 object_getClass func(obj ID) Class40 object_getClass func(obj ID) Class
41+ object_getIvar func(obj ID, ivar Ivar) ID
42+ object_setIvar func(obj ID, ivar Ivar, value ID)
43+ protocol_getName func(protocol *Protocol) string
44+ protocol_isEqual func(p *Protocol, p2 *Protocol) bool
38 )45 )
39 46
40 func init() {47 func init() {
@@ -65,6 +72,11 @@ func init() {
65 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")72 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
66 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")73 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
67 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")74 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
75+ purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName")
76+ purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
77+ purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
78+ purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
79+ purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
68 }80 }
69 81
70 // ID is an opaque pointer to some Objective-C object82 // ID is an opaque pointer to some Objective-C object
@@ -82,6 +94,16 @@ func (id ID) Send(sel SEL, args ...interface{}) ID {
82 return objc_msgSend(id, sel, args...)94 return objc_msgSend(id, sel, args...)
83 }95 }
84 96
97+// GetIvar reads the value of an instance variable in an object.
98+func (id ID) GetIvar(ivar Ivar) ID {
99+ return object_getIvar(id, ivar)
100+}
101+
102+// SetIvar sets the value of an instance variable in an object.
103+func (id ID) SetIvar(ivar Ivar, value ID) {
104+ object_setIvar(id, ivar, value)
105+}
106+
85 // Send is a convenience method for sending messages to objects that can return any type.107 // 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.108 // 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.109 // It is best to cache the result of RegisterName.
@@ -103,7 +125,7 @@ type objc_super struct {
103 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result125 // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
104 // of RegisterName.126 // of RegisterName.
105 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {127 func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
106- var super = &objc_super{128+ super := &objc_super{
107 receiver: id,129 receiver: id,
108 superClass: id.Class(),130 superClass: id.Class(),
109 }131 }
@@ -114,7 +136,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.136 // 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.137 // It is best to cache the result of RegisterName.
116 func SendSuper[T any](id ID, sel SEL, args ...any) T {138 func SendSuper[T any](id ID, sel SEL, args ...any) T {
117- var super = &objc_super{139+ super := &objc_super{
118 receiver: id,140 receiver: id,
119 superClass: id.Class(),141 superClass: id.Class(),
120 }142 }
@@ -148,203 +170,164 @@ func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
148 return objc_allocateClassPair(super, name, extraBytes)170 return objc_allocateClassPair(super, name, extraBytes)
149 }171 }
150 172
151-// Selector is an interface that takes a Go method name173+// MethodDef represents the Go function and the selector that ObjC uses to access that function.
152-// and returns the selector equivalent name.174+type MethodDef struct {
153-// If it returns a nil SEL then that method175+ Cmd SEL
154-// is not added to the class object.176+ Fn any
155-type Selector interface {
156- Selector(string) SEL
157 }177 }
158 178
159-// TagFormatError occurs when the parser fails to parse the objc tag in a Selector object179+// 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, ...>""`)180+// generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes.
181+// The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib.
182+//
183+// Take for example this Objective-C code:
184+//
185+// @property (readwrite) float value;
186+//
187+// In Go, the functions can be accessed as followed:
188+//
189+// var value = purego.Send[float32](id, purego.RegisterName("value"))
190+// id.Send(purego.RegisterName("setValue:"), 3.46)
191+//
192+// [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html
193+type IvarAttrib int
161 194
162-// MismatchError occurs when the Go struct definition doesn't match that of Objective-C195+const (
163-var MismatchError = errors.New("go struct doesn't match objective-c struct")196+ ReadOnly IvarAttrib = 1 << iota
197+ ReadWrite
198+)
164 199
165-// RegisterClass takes a pointer to a struct that implements the Selector interface.200+// 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-C201+// 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 add202+// 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 because203+// The name also cannot contain any spaces.
169-// the size of the struct does not match the size in Objective-C. If no errors occur204+// The type is the Go equivalent type of the Ivar.
170-// then the returned Class has been registered successfully.205+// Attribute determines if a getter and or setter method is generated for this field.
171-//206+type FieldDef struct {
172-// The struct's first field must be of type Class and have a tag that matches the format207+ Name string
173-// `objc:"ClassName : SuperClassName <Protocol, ...>`. This tag is equal to how the class208+ Type reflect.Type
174-// would be defined in Objective-C.209+ Attribute IvarAttrib
175-func RegisterClass(object Selector) (Class, error) {210+}
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 211
215- skipSpace()212+// ivarRegex checks to make sure the Ivar is correctly formatted
213+var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*")
216 214
217- // check for ':'215+// RegisterClass takes the name of the class to create, the superclass, a list of protocols this class
218- if len(tag) > 0 && tag[0] != ':' {216+// 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)217+// describing what went wrong.
220- }218+func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {
221- tag = tag[1:] // skip ':'219+ 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 {220 if class == 0 {
282- return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])221+ return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
283 }222 }
284- if len(split) > 2 {223+ // Add Protocols
285- // Add Protocols224+ for _, p := range protocols {
286- for _, n := range split[2:] {225+ if !class.AddProtocol(p) {
287- if !class.AddProtocol(GetProtocol(n)) {226+ 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 }227 }
291 }228 }
292- // Add exported methods based on the selectors returned from Selector(string) SEL229+ // Add exported methods based on the selectors returned from ClassDef(string) SEL
293- for i := 0; i < ptr.NumMethod(); i++ {230+ 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) {231 imp, err := func() (imp IMP, err error) {
306 defer func() {232 defer func() {
307 if r := recover(); r != nil {233 if r := recover(); r != nil {
308 err = fmt.Errorf("objc: failed to create IMP: %s", r)234 err = fmt.Errorf("objc: failed to create IMP: %s", r)
309 }235 }
310 }()236 }()
311- return NewIMP(fn), nil237+ return NewIMP(def.Fn), nil
312 }()238 }()
313 if err != nil {239 if err != nil {
314- return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)240+ return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
315 }241 }
316- encoding, err := encodeFunc(fn)242+ encoding, err := encodeFunc(def.Fn)
317 if err != nil {243 if err != nil {
318- return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)244+ return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
319 }245 }
320- if !class.AddMethod(sel, imp, encoding) {246+ if !class.AddMethod(def.Cmd, imp, encoding) {
321- return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)247+ return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
322 }248 }
323 }249 }
324 // Add Ivars250 // Add Ivars
325- // Start at 1 because we skip the class object which is first251+ for _, instVar := range ivars {
326- for i := 1; i < strct.NumField(); i++ {252+ ivar := instVar
327- f := strct.Field(i)253+ if !ivarRegex.MatchString(ivar.Name) {
328- if f.Name == "_" {254+ return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name)
329- continue
330 }255 }
331- size := f.Type.Size()256+ size := ivar.Type.Size()
332- alignment := uint8(math.Log2(float64(f.Type.Align())))257+ alignment := uint8(math.Log2(float64(ivar.Type.Align())))
333- enc, err := encodeType(f.Type, false)258+ enc, err := encodeType(ivar.Type, false)
334 if err != nil {259 if err != nil {
335- return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)260+ return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
336 }261 }
337- if !class_addIvar(class, f.Name, size, alignment, enc) {262+ if !class_addIvar(class, ivar.Name, size, alignment, enc) {
338- return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)263+ return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
339 }264 }
340- if offset := class.InstanceVariable(f.Name).Offset(); offset != f.Offset {265+ offset := class.InstanceVariable(ivar.Name).Offset()
341- return 0, fmt.Errorf("objc: couldn't add Ivar %s because offset (%d != %d)", f.Name, offset, f.Offset)266+ switch ivar.Attribute {
267+ case ReadWrite:
268+ ty := reflect.FuncOf(
269+ []reflect.Type{
270+ reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type,
271+ },
272+ nil, false,
273+ )
274+ var encoding string
275+ if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
276+ return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
277+ }
278+ val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
279+ // on entry the first and second arguments are ID and SEL followed by the value
280+ if len(args) != 3 {
281+ panic(fmt.Sprintf("objc: incorrect number of args. expected 3 got %d", len(args)))
282+ }
283+ // The following reflect code does the equivalent of this:
284+ //
285+ // ((*struct {
286+ // Padding [offset]byte
287+ // Value int
288+ // })(unsafe.Pointer(args[0].Interface().(ID)))).v = 123
289+ //
290+ // However, since the type of the variable is unknown reflection is used to actually assign the value
291+ id := args[0].Interface().(ID)
292+ ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
293+ reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem().Set(args[2])
294+ return nil
295+ }).Interface()
296+ // this code only works for ascii but that shouldn't be a problem
297+ selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00"
298+ class.AddMethod(RegisterName(selector), NewIMP(val), encoding)
299+ fallthrough // also implement the read method
300+ case ReadOnly:
301+ ty := reflect.FuncOf(
302+ []reflect.Type{
303+ reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)),
304+ },
305+ []reflect.Type{ivar.Type}, false,
306+ )
307+ var encoding string
308+ if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
309+ return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
310+ }
311+ val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
312+ // on entry the first and second arguments are ID and SEL
313+ if len(args) != 2 {
314+ panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args)))
315+ }
316+ id := args[0].Interface().(ID)
317+ ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
318+ // the variable is located at an offset from the id
319+ return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem()}
320+ }).Interface()
321+ if ivar.Type.Kind() == reflect.Bool {
322+ // this code only works for ascii but that shouldn't be a problem
323+ ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:]
324+ }
325+ class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding)
326+ default:
327+ return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute)
342 }328 }
343 }329 }
344 objc_registerClassPair(class)330 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, nil331 return class, nil
349 }332 }
350 333
@@ -420,7 +403,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
420 if insidePtr {403 if insidePtr {
421 return encStructBegin + typ.Name() + encStructEnd, nil404 return encStructBegin + typ.Name() + encStructEnd, nil
422 }405 }
423- var encoding = encStructBegin406+ encoding := encStructBegin
424 encoding += typ.Name()407 encoding += typ.Name()
425 encoding += "="408 encoding += "="
426 for i := 0; i < typ.NumField(); i++ {409 for i := 0; i < typ.NumField(); i++ {
@@ -543,20 +526,29 @@ func (i Ivar) Offset() uintptr {
543 return ivar_getOffset(i)526 return ivar_getOffset(i)
544 }527 }
545 528
529+func (i Ivar) Name() string {
530+ return ivar_getName(i)
531+}
532+
546 // Protocol is a type that declares methods that can be implemented by any class.533 // Protocol is a type that declares methods that can be implemented by any class.
547-type Protocol uintptr534+type Protocol [0]func()
548 535
549 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.536 // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
550 func GetProtocol(name string) *Protocol {537 func GetProtocol(name string) *Protocol {
551 return objc_getProtocol(name)538 return objc_getProtocol(name)
552 }539 }
553 540
541+// Equals return true if the two protocols are the same.
542+func (p *Protocol) Equals(p2 *Protocol) bool {
543+ return protocol_isEqual(p, p2)
544+}
545+
554 // IMP is a function pointer that can be called by Objective-C code.546 // IMP is a function pointer that can be called by Objective-C code.
555 type IMP uintptr547 type IMP uintptr
556 548
557 // NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.549 // 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.550 // It returns an IMP function pointer that can be called by Objective-C code.
551+// The function panics if an error occurs.
560 // The function pointer is never deallocated.552 // The function pointer is never deallocated.
561 func NewIMP(fn interface{}) IMP {553 func NewIMP(fn interface{}) IMP {
562 ty := reflect.TypeOf(fn)554 ty := reflect.TypeOf(fn)
@@ -568,13 +560,9 @@ func NewIMP(fn interface{}) IMP {
568 switch {560 switch {
569 case ty.NumIn() < 2:561 case ty.NumIn() < 2:
570 fallthrough562 fallthrough
571- case ty.In(0).Kind() != reflect.Uintptr && // checks if it's objc.ID563+ 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 fallthrough564 fallthrough
577- case ty.In(1).Kind() != reflect.Uintptr:565+ 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())566 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
579 }567 }
580 return IMP(purego.NewCallback(fn))568 return IMP(purego.NewCallback(fn))
M objc/objc_runtime_darwin_test.go +46 -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,61 @@ 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+ Name: "foo",
53+ Type: reflect.TypeOf(false),
54+ Attribute: objc.ReadWrite,
55+ },
56+ },
57+ []objc.MethodDef{
58+ {
59+ Cmd: sel_init,
60+ Fn: BarInit,
61+ },
62+ },
63+ )
64+ if err != nil {
65+ panic(err)
3066 }
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()
4067
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")))
68+ object := objc.ID(class).Send(sel_new)
69+ object.Send(sel_setBar, 123)
70+ bar := int(object.Send(sel_bar))
4471 fmt.Println(bar)
4572 // Output: 123
4673 }
@@ -85,7 +112,7 @@ func TestSend(t *testing.T) {
85112 // Initialize a NSNumber
86113 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)
87114 // Then get that number back using the generic Send function.
88- var number = objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
115+ number := objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
89116 if double != number {
90117 t.Failed()
91118 }
@@ -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,61 @@ 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+ Name: "foo",
53+ Type: reflect.TypeOf(false),
54+ Attribute: objc.ReadWrite,
55+ },
56+ },
57+ []objc.MethodDef{
58+ {
59+ Cmd: sel_init,
60+ Fn: BarInit,
61+ },
62+ },
63+ )
64+ if err != nil {
65+ panic(err)
30 }66 }
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 67
41- var object = objc.ID(class).Send(objc.RegisterName("new"))68+ object := objc.ID(class).Send(sel_new)
42- object.Send(objc.RegisterName("setBar:"), 123)69+ object.Send(sel_setBar, 123)
43- var bar = int(object.Send(objc.RegisterName("bar")))70+ bar := int(object.Send(sel_bar))
44 fmt.Println(bar)71 fmt.Println(bar)
45 // Output: 12372 // Output: 123
46 }73 }
@@ -85,7 +112,7 @@ func TestSend(t *testing.T) {
85 // Initialize a NSNumber112 // Initialize a NSNumber
86 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)113 NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double)
87 // Then get that number back using the generic Send function.114 // Then get that number back using the generic Send function.
88- var number = objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))115+ number := objc.Send[float64](NSNumber, objc.RegisterName("doubleValue"))
89 if double != number {116 if double != number {
90 t.Failed()117 t.Failed()
91 }118 }

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