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, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)
42 if err != nil {
43 panic(fmt.Errorf("objc: %w", err))
44 }
45 objc_msgSend_fn, err = purego.Dlsym(objc, "objc_msgSend")
46 if err != nil {
47 panic(fmt.Errorf("objc: %w", err))
48 }
49 purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn)
50 objc_msgSendSuper2_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2")
51 if err != nil {
52 panic(fmt.Errorf("objc: %w", err))
53 }
54 purego.RegisterFunc(&objc_msgSendSuper2, objc_msgSendSuper2_fn)
55 purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
56 purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
57 purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
58 purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
59 purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
60 purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
61 purego.RegisterLibFunc(&class_getSuperclass, objc, "class_getSuperclass")
62 purego.RegisterLibFunc(&class_getInstanceVariable, objc, "class_getInstanceVariable")
63 purego.RegisterLibFunc(&class_addMethod, objc, "class_addMethod")
64 purego.RegisterLibFunc(&class_addIvar, objc, "class_addIvar")
65 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
66 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
67 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
68}
69
70// ID is an opaque pointer to some Objective-C object
71type ID uintptr
72
73// Class returns the class of the object.
74func (id ID) Class() Class {
75 return object_getClass(id)
76}
77
78// Send is a convenience method for sending messages to objects. This function takes a SEL
79// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
80// of RegisterName.
81func (id ID) Send(sel SEL, args ...interface{}) ID {
82 return objc_msgSend(id, sel, args...)
83}
84
85// 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.
87// It is best to cache the result of RegisterName.
88func Send[T any](id ID, sel SEL, args ...any) T {
89 var fn func(id ID, sel SEL, args ...any) T
90 purego.RegisterFunc(&fn, objc_msgSend_fn)
91 return fn(id, sel, args...)
92}
93
94// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
95// as the receiver of a message. It specifies the class definition of the particular superclass that should
96// be messaged.
97type objc_super struct {
98 receiver ID
99 superClass Class
100}
101
102// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
103// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
104// of RegisterName.
105func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
106 var super = &objc_super{
107 receiver: id,
108 superClass: id.Class(),
109 }
110 return objc_msgSendSuper2(super, sel, args...)
111}
112
113// SendSuper is a convenience method for sending message to object's super that can return any type.
114// 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.
116func SendSuper[T any](id ID, sel SEL, args ...any) T {
117 var super = &objc_super{
118 receiver: id,
119 superClass: id.Class(),
120 }
121 var fn func(objcSuper *objc_super, sel SEL, args ...any) T
122 purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
123 return fn(super, sel, args...)
124}
125
126// SEL is an opaque type that represents a method selector
127type SEL uintptr
128
129// RegisterName registers a method with the Objective-C runtime system, maps the method name to a selector,
130// and returns the selector value. This function grabs the global Objective-c lock. It is best the cache the
131// result of this function.
132func RegisterName(name string) SEL {
133 return sel_registerName(name)
134}
135
136// Class is an opaque type that represents an Objective-C class.
137type Class uintptr
138
139// GetClass returns the Class object for the named class, or nil if the class is not registered with the Objective-C runtime.
140func GetClass(name string) Class {
141 return objc_getClass(name)
142}
143
144// AllocateClassPair creates a new class and metaclass. Then returns the new class, or Nil if the class could not be created
145//
146// Deprecated: use RegisterClass instead
147func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
148 return objc_allocateClassPair(super, name, extraBytes)
149}
150
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.
155type Selector interface {
156 Selector(string) SEL
157}
158
159// TagFormatError occurs when the parser fails to parse the objc tag in a Selector object
160var TagFormatError = errors.New(`objc tag doesn't match "ClassName : SuperClassName <Protocol, ...>""`)
161
162// MismatchError occurs when the Go struct definition doesn't match that of Objective-C
163var MismatchError = errors.New("go struct doesn't match objective-c struct")
164
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.
175func 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:]
214
215 skipSpace()
216
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)
281 if class == 0 {
282 return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])
283 }
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 }
290 }
291 }
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()
305 imp, err := func() (imp IMP, err error) {
306 defer func() {
307 if r := recover(); r != nil {
308 err = fmt.Errorf("objc: failed to create IMP: %s", r)
309 }
310 }()
311 return NewIMP(fn), nil
312 }()
313 if err != nil {
314 return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
315 }
316 encoding, err := encodeFunc(fn)
317 if err != nil {
318 return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
319 }
320 if !class.AddMethod(sel, imp, encoding) {
321 return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)
322 }
323 }
324 // 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
330 }
331 size := f.Type.Size()
332 alignment := uint8(math.Log2(float64(f.Type.Align())))
333 enc, err := encodeType(f.Type, false)
334 if err != nil {
335 return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)
336 }
337 if !class_addIvar(class, f.Name, size, alignment, enc) {
338 return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)
339 }
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)
342 }
343 }
344 objc_registerClassPair(class)
345 if size1, size2 := class.InstanceSize(), strct.Size(); size1 != size2 {
346 return 0, fmt.Errorf("objc: sizes don't match %d != %d: %w", size1, size2, MismatchError)
347 }
348 return class, nil
349}
350
351const (
352 encId = "@"
353 encClass = "#"
354 encSelector = ":"
355 encChar = "c"
356 encUChar = "C"
357 encShort = "s"
358 encUShort = "S"
359 encInt = "i"
360 encUInt = "I"
361 encLong = "l"
362 encULong = "L"
363 encFloat = "f"
364 encDouble = "d"
365 encBool = "B"
366 encVoid = "v"
367 encPtr = "^"
368 encCharPtr = "*"
369 encStructBegin = "{"
370 encStructEnd = "}"
371 encUnsafePtr = "^v"
372)
373
374// encodeType returns a string representing a type as if it was given to @encode(typ)
375// Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
376func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
377 switch typ {
378 case reflect.TypeOf(Class(0)):
379 return encClass, nil
380 case reflect.TypeOf(ID(0)):
381 return encId, nil
382 case reflect.TypeOf(SEL(0)):
383 return encSelector, nil
384 }
385
386 kind := typ.Kind()
387 switch kind {
388 case reflect.Bool:
389 return encBool, nil
390 case reflect.Int:
391 return encLong, nil
392 case reflect.Int8:
393 return encChar, nil
394 case reflect.Int16:
395 return encShort, nil
396 case reflect.Int32:
397 return encInt, nil
398 case reflect.Int64:
399 return encULong, nil
400 case reflect.Uint:
401 return encULong, nil
402 case reflect.Uint8:
403 return encUChar, nil
404 case reflect.Uint16:
405 return encUShort, nil
406 case reflect.Uint32:
407 return encUInt, nil
408 case reflect.Uint64:
409 return encULong, nil
410 case reflect.Uintptr:
411 return encPtr, nil
412 case reflect.Float32:
413 return encFloat, nil
414 case reflect.Float64:
415 return encDouble, nil
416 case reflect.Ptr:
417 enc, err := encodeType(typ.Elem(), true)
418 return encPtr + enc, err
419 case reflect.Struct:
420 if insidePtr {
421 return encStructBegin + typ.Name() + encStructEnd, nil
422 }
423 var encoding = encStructBegin
424 encoding += typ.Name()
425 encoding += "="
426 for i := 0; i < typ.NumField(); i++ {
427 f := typ.Field(i)
428 tmp, err := encodeType(f.Type, false)
429 if err != nil {
430 return "", err
431 }
432 encoding += tmp
433 }
434 encoding = encStructEnd
435 return encoding, nil
436 case reflect.UnsafePointer:
437 return encUnsafePtr, nil
438 case reflect.String:
439 return encCharPtr, nil
440 }
441
442 return "", errors.New(fmt.Sprintf("unhandled/invalid kind %v typed %v", kind, typ))
443}
444
445// encodeFunc returns a functions type as if it was given to @encode(fn)
446func encodeFunc(fn interface{}) (string, error) {
447 typ := reflect.TypeOf(fn)
448 if typ.Kind() != reflect.Func {
449 return "", errors.New("not a func")
450 }
451
452 encoding := ""
453 switch typ.NumOut() {
454 case 0:
455 encoding += encVoid
456 case 1:
457 tmp, err := encodeType(typ.Out(0), false)
458 if err != nil {
459 return "", err
460 }
461 encoding += tmp
462 default:
463 return "", errors.New("too many output parameters")
464 }
465
466 if typ.NumIn() < 2 {
467 return "", errors.New("func doesn't take ID and SEL as its first two parameters")
468 }
469
470 encoding += encId
471
472 for i := 1; i < typ.NumIn(); i++ {
473 tmp, err := encodeType(typ.In(i), false)
474 if err != nil {
475 return "", err
476 }
477 encoding += tmp
478 }
479 return encoding, nil
480}
481
482// SuperClass returns the superclass of a class.
483// You should usually use NSObject‘s superclass method instead of this function.
484func (c Class) SuperClass() Class {
485 return class_getSuperclass(c)
486}
487
488// AddMethod adds a new method to a class with a given name and implementation.
489// The types argument is a string containing the mapping of parameters and return type.
490// Since the function must take at least two arguments—self and _cmd, the second and third
491// characters must be “@:” (the first character is the return type).
492func (c Class) AddMethod(name SEL, imp IMP, types string) bool {
493 return class_addMethod(c, name, imp, types)
494}
495
496// AddIvar adds a new instance variable to a class.
497// It may only be called after AllocateClassPair and before Register.
498// Adding an instance variable to an existing class is not supported.
499// The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
500// It takes the instance of the type of the Ivar and a string representing the type.
501//
502// Deprecated: use RegisterClass instead
503func (c Class) AddIvar(name string, ty interface{}, types string) bool {
504 typeOf := reflect.TypeOf(ty)
505 size := typeOf.Size()
506 alignment := uint8(math.Log2(float64(typeOf.Align())))
507 return class_addIvar(c, name, size, alignment, types)
508}
509
510// AddProtocol adds a protocol to a class.
511// Returns true if the protocol was added successfully, otherwise false (for example,
512// the class already conforms to that protocol).
513func (c Class) AddProtocol(protocol *Protocol) bool {
514 return class_addProtocol(c, protocol)
515}
516
517// InstanceSize returns the size in bytes of instances of the class or 0 if cls is nil
518func (c Class) InstanceSize() uintptr {
519 return class_getInstanceSize(c)
520}
521
522// InstanceVariable returns an Ivar data structure containing information about the instance variable specified by name.
523func (c Class) InstanceVariable(name string) Ivar {
524 return class_getInstanceVariable(c, name)
525}
526
527// Register registers a class that was allocated using AllocateClassPair.
528// It can now be used to make objects by sending it either alloc and init or new.
529//
530// Deprecated: use RegisterClass instead
531func (c Class) Register() {
532 objc_registerClassPair(c)
533}
534
535// Ivar an opaque type that represents an instance variable.
536type Ivar uintptr
537
538// Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value.
539//
540// For instance variables of type ID or other object types, call Ivar and SetIvar instead
541// of using this offset to access the instance variable data directly.
542func (i Ivar) Offset() uintptr {
543 return ivar_getOffset(i)
544}
545
546// Protocol is a type that declares methods that can be implemented by any class.
547type Protocol uintptr
548
549// GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
550func GetProtocol(name string) *Protocol {
551 return objc_getProtocol(name)
552}
553
554// IMP is a function pointer that can be called by Objective-C code.
555type IMP uintptr
556
557// NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
558// ID may instead be a pointer to a struct whose first field has type Class.
559// It returns an IMP function pointer that can be called by Objective-C code.
560// The function pointer is never deallocated.
561func NewIMP(fn interface{}) IMP {
562 ty := reflect.TypeOf(fn)
563 if ty.Kind() != reflect.Func {
564 panic("objc: not a function")
565 }
566 // IMP is stricter than a normal callback
567 // id (*IMP)(id, SEL, ...)
568 switch {
569 case ty.NumIn() < 2:
570 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))):
576 fallthrough
577 case ty.In(1).Kind() != reflect.Uintptr:
578 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
579 }
580 return IMP(purego.NewCallback(fn))
581}