1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3
4// Package objc is a low-level pure Go objective-c runtime. This package is easy to use incorrectly, so it is best
5// to use a wrapper that provides the functionality you need in a safer way.
6package objc
7
8import (
9 "errors"
10 "fmt"
11 "math"
12 "reflect"
13
14 "github.com/ebitengine/purego"
15)
16
17//TODO: support try/catch?
18//https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
19
20var (
21 objc_msgSend_fn uintptr
22 objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID
23 objc_msgSendSuper2_fn uintptr
24 objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...interface{}) ID
25 objc_getClass func(name string) Class
26 objc_getProtocol func(name string) *Protocol
27 objc_allocateClassPair func(super Class, name string, extraBytes uintptr) Class
28 objc_registerClassPair func(class Class)
29 sel_registerName func(name string) SEL
30 class_getSuperclass func(class Class) Class
31 class_getInstanceVariable func(class Class, name string) Ivar
32 class_getInstanceSize func(class Class) uintptr
33 class_addMethod func(class Class, name SEL, imp IMP, types string) bool
34 class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool
35 class_addProtocol func(class Class, protocol *Protocol) bool
36 ivar_getOffset func(ivar Ivar) uintptr
37 object_getClass func(obj ID) Class
38)
39
40func init() {
41 objc := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)
42 if err := purego.Dlerror(); err != "" {
43 panic("objc: " + err)
44 }
45 objc_msgSend_fn = purego.Dlsym(objc, "objc_msgSend")
46 purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn)
47 objc_msgSendSuper2_fn = purego.Dlsym(objc, "objc_msgSendSuper2")
48 purego.RegisterFunc(&objc_msgSendSuper2, objc_msgSendSuper2_fn)
49 purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
50 purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
51 purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
52 purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
53 purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
54 purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
55 purego.RegisterLibFunc(&class_getSuperclass, objc, "class_getSuperclass")
56 purego.RegisterLibFunc(&class_getInstanceVariable, objc, "class_getInstanceVariable")
57 purego.RegisterLibFunc(&class_addMethod, objc, "class_addMethod")
58 purego.RegisterLibFunc(&class_addIvar, objc, "class_addIvar")
59 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
60 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
61 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
62}
63
64// ID is an opaque pointer to some Objective-C object
65type ID uintptr
66
67// Class returns the class of the object.
68func (id ID) Class() Class {
69 return object_getClass(id)
70}
71
72// Send is a convenience method for sending messages to objects. This function takes a SEL
73// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
74// of RegisterName.
75func (id ID) Send(sel SEL, args ...interface{}) ID {
76 return objc_msgSend(id, sel, args...)
77}
78
79// Send is a convenience method for sending messages to objects that can return any type.
80// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
81// It is best to cache the result of RegisterName.
82func Send[T any](id ID, sel SEL, args ...any) T {
83 var fn func(id ID, sel SEL, args ...any) T
84 purego.RegisterFunc(&fn, objc_msgSend_fn)
85 return fn(id, sel, args...)
86}
87
88// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
89// as the receiver of a message. It specifies the class definition of the particular superclass that should
90// be messaged.
91type objc_super struct {
92 receiver ID
93 superClass Class
94}
95
96// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
97// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
98// of RegisterName.
99func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
100 var super = &objc_super{
101 receiver: id,
102 superClass: id.Class(),
103 }
104 return objc_msgSendSuper2(super, sel, args...)
105}
106
107// SendSuper is a convenience method for sending message to object's super that can return any type.
108// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
109// It is best to cache the result of RegisterName.
110func SendSuper[T any](id ID, sel SEL, args ...any) T {
111 var super = &objc_super{
112 receiver: id,
113 superClass: id.Class(),
114 }
115 var fn func(objcSuper *objc_super, sel SEL, args ...any) T
116 purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
117 return fn(super, sel, args...)
118}
119
120// SEL is an opaque type that represents a method selector
121type SEL uintptr
122
123// RegisterName registers a method with the Objective-C runtime system, maps the method name to a selector,
124// and returns the selector value. This function grabs the global Objective-c lock. It is best the cache the
125// result of this function.
126func RegisterName(name string) SEL {
127 return sel_registerName(name)
128}
129
130// Class is an opaque type that represents an Objective-C class.
131type Class uintptr
132
133// GetClass returns the Class object for the named class, or nil if the class is not registered with the Objective-C runtime.
134func GetClass(name string) Class {
135 return objc_getClass(name)
136}
137
138// AllocateClassPair creates a new class and metaclass. Then returns the new class, or Nil if the class could not be created
139//
140// Deprecated: use RegisterClass instead
141func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
142 return objc_allocateClassPair(super, name, extraBytes)
143}
144
145// Selector is an interface that takes a Go method name
146// and returns the selector equivalent name.
147// If it returns a nil SEL then that method
148// is not added to the class object.
149type Selector interface {
150 Selector(string) SEL
151}
152
153// TagFormatError occurs when the parser fails to parse the objc tag in a Selector object
154var TagFormatError = errors.New(`objc tag doesn't match "ClassName : SuperClassName <Protocol, ...>""`)
155
156// MismatchError occurs when the Go struct definition doesn't match that of Objective-C
157var MismatchError = errors.New("go struct doesn't match objective-c struct")
158
159// RegisterClass takes a pointer to a struct that implements the Selector interface.
160// It will register the structs fields and pointer receiver methods in the Objective-C
161// runtime using the SEL returned from Selector. Any errors that occur trying to add
162// a Method or Ivar is returned as an error. Such errors may occur in parsing or because
163// the size of the struct does not match the size in Objective-C. If no errors occur
164// then the returned Class has been registered successfully.
165//
166// The struct's first field must be of type Class and have a tag that matches the format
167// `objc:"ClassName : SuperClassName <Protocol, ...>`. This tag is equal to how the class
168// would be defined in Objective-C.
169func RegisterClass(object Selector) (Class, error) {
170 ptr := reflect.TypeOf(object)
171 strct := ptr.Elem()
172 if strct.NumField() == 0 || strct.Field(0).Type != reflect.TypeOf(Class(0)) {
173 return 0, fmt.Errorf("objc: need objc.Class as first field: %w", MismatchError)
174 }
175 isa := strct.Field(0)
176 tag := isa.Tag.Get("objc")
177 if tag == "" {
178 return 0, fmt.Errorf("objc: missing objc tag: %w", TagFormatError)
179 }
180 // split contains the class name and super class name followed by all the Protocols
181 // start with two for ClassName : SuperClassName
182 var split = make([]string, 2)
183 {
184 // This is a simple parser for the objc tag that looks for the format
185 // "ClassName : SuperClassName <Protocol, ...>"
186 // It appends to the split variable with the [ClassName, SuperClassName, Protocol, ...]
187
188 var i int // from tag[0:i] is whatever identifier is next
189 var r rune // r is the current rune
190 skipSpace := func() {
191 for _, c := range tag {
192 if c == ' ' {
193 tag = tag[1:]
194 continue
195 }
196 break
197 }
198 }
199 skipSpace()
200 // get ClassName
201 for i, r = range tag {
202 if r == ' ' || r == ':' {
203 break
204 }
205 }
206 split[0] = tag[0:i] // store ClassName
207 tag = tag[i:]
208
209 skipSpace()
210
211 // check for ':'
212 if len(tag) > 0 && tag[0] != ':' {
213 return 0, fmt.Errorf("objc: missing ':': %w", TagFormatError)
214 }
215 tag = tag[1:] // skip ':'
216 skipSpace()
217
218 // get SuperClassName
219 for i, r = range tag {
220 if r == ' ' {
221 break
222 } else if i+1 == len(tag) {
223 // if this is the last character in the string
224 // make sure to increment i so that tag[:i]
225 // includes the last character
226 i++
227 break
228 }
229 }
230 if len(tag) < i {
231 return 0, fmt.Errorf("objc: missing SuperClassName: %w", TagFormatError)
232 }
233 split[1] = tag[:i] // store SuperClassName
234 tag = tag[i:] // drop SuperClassName
235 skipSpace()
236 if len(tag) > 0 {
237 if tag[0] != '<' {
238 return 0, fmt.Errorf("objc: expected '<': %w", TagFormatError)
239 }
240 tag = tag[1:] // drop '<'
241 // get Protocols
242 outer:
243 for {
244 skipSpace()
245 for i, r = range tag {
246 switch r {
247 case ' ':
248 split = append(split, tag[:i])
249 tag = tag[i:]
250 continue outer
251 case ',':
252 // If there is actually an identifier - add it.
253 if i > 0 {
254 split = append(split, tag[:i])
255 tag = tag[i:]
256 } else {
257 // Otherwise, drop ','
258 tag = tag[1:]
259 }
260 continue outer
261 case '>':
262 // If there is actually an identifier - add it.
263 if i > 0 {
264 split = append(split, tag[:i])
265 tag = tag[i:]
266 }
267 break outer
268 }
269 }
270 return 0, fmt.Errorf("objc: expected '>': %w", TagFormatError)
271 }
272 }
273 }
274 class := objc_allocateClassPair(GetClass(split[1]), split[0], 0)
275 if class == 0 {
276 return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])
277 }
278 if len(split) > 2 {
279 // Add Protocols
280 for _, n := range split[2:] {
281 if !class.AddProtocol(GetProtocol(n)) {
282 return 0, fmt.Errorf("objc: couldn't add Protocol %s", n)
283 }
284 }
285 }
286 // Add exported methods based on the selectors returned from Selector(string) SEL
287 for i := 0; i < ptr.NumMethod(); i++ {
288 met := ptr.Method(i)
289 // we know this method is the interface one since RegisterClass
290 // requires that the struct implement Selector.
291 if met.Name == "Selector" {
292 continue
293 }
294 sel := object.Selector(met.Name)
295 if sel == 0 {
296 continue
297 }
298 fn := met.Func.Interface()
299 imp, err := func() (imp IMP, err error) {
300 defer func() {
301 if r := recover(); r != nil {
302 err = fmt.Errorf("objc: failed to create IMP: %s", r)
303 }
304 }()
305 return NewIMP(fn), nil
306 }()
307 if err != nil {
308 return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
309 }
310 encoding, err := encodeFunc(fn)
311 if err != nil {
312 return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
313 }
314 if !class.AddMethod(sel, imp, encoding) {
315 return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)
316 }
317 }
318 // Add Ivars
319 // Start at 1 because we skip the class object which is first
320 for i := 1; i < strct.NumField(); i++ {
321 f := strct.Field(i)
322 if f.Name == "_" {
323 continue
324 }
325 size := f.Type.Size()
326 alignment := uint8(math.Log2(float64(f.Type.Align())))
327 enc, err := encodeType(f.Type, false)
328 if err != nil {
329 return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)
330 }
331 if !class_addIvar(class, f.Name, size, alignment, enc) {
332 return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)
333 }
334 if offset := class.InstanceVariable(f.Name).Offset(); offset != f.Offset {
335 return 0, fmt.Errorf("objc: couldn't add Ivar %s because offset (%d != %d)", f.Name, offset, f.Offset)
336 }
337 }
338 objc_registerClassPair(class)
339 if size1, size2 := class.InstanceSize(), strct.Size(); size1 != size2 {
340 return 0, fmt.Errorf("objc: sizes don't match %d != %d: %w", size1, size2, MismatchError)
341 }
342 return class, nil
343}
344
345const (
346 encId = "@"
347 encClass = "#"
348 encSelector = ":"
349 encChar = "c"
350 encUChar = "C"
351 encShort = "s"
352 encUShort = "S"
353 encInt = "i"
354 encUInt = "I"
355 encLong = "l"
356 encULong = "L"
357 encFloat = "f"
358 encDouble = "d"
359 encBool = "B"
360 encVoid = "v"
361 encPtr = "^"
362 encCharPtr = "*"
363 encStructBegin = "{"
364 encStructEnd = "}"
365 encUnsafePtr = "^v"
366)
367
368// encodeType returns a string representing a type as if it was given to @encode(typ)
369// Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
370func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
371 switch typ {
372 case reflect.TypeOf(Class(0)):
373 return encClass, nil
374 case reflect.TypeOf(ID(0)):
375 return encId, nil
376 case reflect.TypeOf(SEL(0)):
377 return encSelector, nil
378 }
379
380 kind := typ.Kind()
381 switch kind {
382 case reflect.Bool:
383 return encBool, nil
384 case reflect.Int:
385 return encLong, nil
386 case reflect.Int8:
387 return encChar, nil
388 case reflect.Int16:
389 return encShort, nil
390 case reflect.Int32:
391 return encInt, nil
392 case reflect.Int64:
393 return encULong, nil
394 case reflect.Uint:
395 return encULong, nil
396 case reflect.Uint8:
397 return encUChar, nil
398 case reflect.Uint16:
399 return encUShort, nil
400 case reflect.Uint32:
401 return encUInt, nil
402 case reflect.Uint64:
403 return encULong, nil
404 case reflect.Uintptr:
405 return encPtr, nil
406 case reflect.Float32:
407 return encFloat, nil
408 case reflect.Float64:
409 return encDouble, nil
410 case reflect.Ptr:
411 enc, err := encodeType(typ.Elem(), true)
412 return encPtr + enc, err
413 case reflect.Struct:
414 if insidePtr {
415 return encStructBegin + typ.Name() + encStructEnd, nil
416 }
417 var encoding = encStructBegin
418 encoding += typ.Name()
419 encoding += "="
420 for i := 0; i < typ.NumField(); i++ {
421 f := typ.Field(i)
422 tmp, err := encodeType(f.Type, false)
423 if err != nil {
424 return "", err
425 }
426 encoding += tmp
427 }
428 encoding = encStructEnd
429 return encoding, nil
430 case reflect.UnsafePointer:
431 return encUnsafePtr, nil
432 case reflect.String:
433 return encCharPtr, nil
434 }
435
436 return "", errors.New(fmt.Sprintf("unhandled/invalid kind %v typed %v", kind, typ))
437}
438
439// encodeFunc returns a functions type as if it was given to @encode(fn)
440func encodeFunc(fn interface{}) (string, error) {
441 typ := reflect.TypeOf(fn)
442 if typ.Kind() != reflect.Func {
443 return "", errors.New("not a func")
444 }
445
446 encoding := ""
447 switch typ.NumOut() {
448 case 0:
449 encoding += encVoid
450 case 1:
451 tmp, err := encodeType(typ.Out(0), false)
452 if err != nil {
453 return "", err
454 }
455 encoding += tmp
456 default:
457 return "", errors.New("too many output parameters")
458 }
459
460 if typ.NumIn() < 2 {
461 return "", errors.New("func doesn't take ID and SEL as its first two parameters")
462 }
463
464 encoding += encId
465
466 for i := 1; i < typ.NumIn(); i++ {
467 tmp, err := encodeType(typ.In(i), false)
468 if err != nil {
469 return "", err
470 }
471 encoding += tmp
472 }
473 return encoding, nil
474}
475
476// SuperClass returns the superclass of a class.
477// You should usually use NSObject‘s superclass method instead of this function.
478func (c Class) SuperClass() Class {
479 return class_getSuperclass(c)
480}
481
482// AddMethod adds a new method to a class with a given name and implementation.
483// The types argument is a string containing the mapping of parameters and return type.
484// Since the function must take at least two arguments—self and _cmd, the second and third
485// characters must be “@:” (the first character is the return type).
486func (c Class) AddMethod(name SEL, imp IMP, types string) bool {
487 return class_addMethod(c, name, imp, types)
488}
489
490// AddIvar adds a new instance variable to a class.
491// It may only be called after AllocateClassPair and before Register.
492// Adding an instance variable to an existing class is not supported.
493// The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
494// It takes the instance of the type of the Ivar and a string representing the type.
495//
496// Deprecated: use RegisterClass instead
497func (c Class) AddIvar(name string, ty interface{}, types string) bool {
498 typeOf := reflect.TypeOf(ty)
499 size := typeOf.Size()
500 alignment := uint8(math.Log2(float64(typeOf.Align())))
501 return class_addIvar(c, name, size, alignment, types)
502}
503
504// AddProtocol adds a protocol to a class.
505// Returns true if the protocol was added successfully, otherwise false (for example,
506// the class already conforms to that protocol).
507func (c Class) AddProtocol(protocol *Protocol) bool {
508 return class_addProtocol(c, protocol)
509}
510
511// InstanceSize returns the size in bytes of instances of the class or 0 if cls is nil
512func (c Class) InstanceSize() uintptr {
513 return class_getInstanceSize(c)
514}
515
516// InstanceVariable returns an Ivar data structure containing information about the instance variable specified by name.
517func (c Class) InstanceVariable(name string) Ivar {
518 return class_getInstanceVariable(c, name)
519}
520
521// Register registers a class that was allocated using AllocateClassPair.
522// It can now be used to make objects by sending it either alloc and init or new.
523//
524// Deprecated: use RegisterClass instead
525func (c Class) Register() {
526 objc_registerClassPair(c)
527}
528
529// Ivar an opaque type that represents an instance variable.
530type Ivar uintptr
531
532// Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value.
533//
534// For instance variables of type ID or other object types, call Ivar and SetIvar instead
535// of using this offset to access the instance variable data directly.
536func (i Ivar) Offset() uintptr {
537 return ivar_getOffset(i)
538}
539
540// Protocol is a type that declares methods that can be implemented by any class.
541type Protocol uintptr
542
543// GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
544func GetProtocol(name string) *Protocol {
545 return objc_getProtocol(name)
546}
547
548// IMP is a function pointer that can be called by Objective-C code.
549type IMP uintptr
550
551// NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
552// ID may instead be a pointer to a struct whose first field has type Class.
553// It returns an IMP function pointer that can be called by Objective-C code.
554// The function pointer is never deallocated.
555func NewIMP(fn interface{}) IMP {
556 ty := reflect.TypeOf(fn)
557 if ty.Kind() != reflect.Func {
558 panic("objc: not a function")
559 }
560 // IMP is stricter than a normal callback
561 // id (*IMP)(id, SEL, ...)
562 switch {
563 case ty.NumIn() < 2:
564 fallthrough
565 case ty.In(0).Kind() != reflect.Uintptr && // checks if it's objc.ID
566 // or that it's a pointer to a struct
567 (ty.In(0).Kind() != reflect.Pointer || ty.In(0).Elem().Kind() != reflect.Struct ||
568 // and that the structs first field is an objc.Class
569 ty.In(0).Elem().Field(0).Type != reflect.TypeOf(Class(0))):
570 fallthrough
571 case ty.In(1).Kind() != reflect.Uintptr:
572 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
573 }
574 return IMP(purego.NewCallback(fn))
575}