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 "regexp"
14 "runtime"
15 "unicode"
16 "unsafe"
17
18 "github.com/ebitengine/purego"
19)
20
21// TODO: support try/catch?
22// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
23var (
24 objc_msgSend_fn uintptr
25 objc_msgSend_stret_fn uintptr
26 objc_msgSend func(obj ID, cmd SEL, args ...any) ID
27 objc_msgSendSuper2_fn uintptr
28 objc_msgSendSuper2_stret_fn uintptr
29 objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...any) ID
30 objc_getClass func(name string) Class
31 objc_getProtocol func(name string) *Protocol
32 objc_allocateClassPair func(super Class, name string, extraBytes uintptr) Class
33 objc_registerClassPair func(class Class)
34 sel_registerName func(name string) SEL
35 class_getSuperclass func(class Class) Class
36 class_getInstanceVariable func(class Class, name string) Ivar
37 class_getInstanceSize func(class Class) uintptr
38 class_addMethod func(class Class, name SEL, imp IMP, types string) bool
39 class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool
40 class_addProtocol func(class Class, protocol *Protocol) bool
41 ivar_getOffset func(ivar Ivar) uintptr
42 ivar_getName func(ivar Ivar) string
43 object_getClass func(obj ID) Class
44 object_getIvar func(obj ID, ivar Ivar) ID
45 object_setIvar func(obj ID, ivar Ivar, value ID)
46 protocol_getName func(protocol *Protocol) string
47 protocol_isEqual func(p *Protocol, p2 *Protocol) bool
48)
49
50func init() {
51 objc, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)
52 if err != nil {
53 panic(fmt.Errorf("objc: %w", err))
54 }
55 objc_msgSend_fn, err = purego.Dlsym(objc, "objc_msgSend")
56 if err != nil {
57 panic(fmt.Errorf("objc: %w", err))
58 }
59 if runtime.GOARCH == "amd64" {
60 objc_msgSend_stret_fn, err = purego.Dlsym(objc, "objc_msgSend_stret")
61 if err != nil {
62 panic(fmt.Errorf("objc: %w", err))
63 }
64 objc_msgSendSuper2_stret_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2_stret")
65 if err != nil {
66 panic(fmt.Errorf("objc: %w", err))
67 }
68 }
69 purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn)
70 objc_msgSendSuper2_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2")
71 if err != nil {
72 panic(fmt.Errorf("objc: %w", err))
73 }
74 purego.RegisterFunc(&objc_msgSendSuper2, objc_msgSendSuper2_fn)
75 purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
76 purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
77 purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
78 purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
79 purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
80 purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
81 purego.RegisterLibFunc(&class_getSuperclass, objc, "class_getSuperclass")
82 purego.RegisterLibFunc(&class_getInstanceVariable, objc, "class_getInstanceVariable")
83 purego.RegisterLibFunc(&class_addMethod, objc, "class_addMethod")
84 purego.RegisterLibFunc(&class_addIvar, objc, "class_addIvar")
85 purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
86 purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
87 purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
88 purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName")
89 purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
90 purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
91 purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
92 purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
93}
94
95// ID is an opaque pointer to some Objective-C object
96type ID uintptr
97
98// Class returns the class of the object.
99func (id ID) Class() Class {
100 return object_getClass(id)
101}
102
103// Send is a convenience method for sending messages to objects. This function takes a SEL
104// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
105// of RegisterName.
106func (id ID) Send(sel SEL, args ...any) ID {
107 return objc_msgSend(id, sel, args...)
108}
109
110// GetIvar reads the value of an instance variable in an object.
111func (id ID) GetIvar(ivar Ivar) ID {
112 return object_getIvar(id, ivar)
113}
114
115// SetIvar sets the value of an instance variable in an object.
116func (id ID) SetIvar(ivar Ivar, value ID) {
117 object_setIvar(id, ivar, value)
118}
119
120// keep in sync with func.go
121const maxRegAllocStructSize = 16
122
123// Send is a convenience method for sending messages to objects that can return any type.
124// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
125// It is best to cache the result of RegisterName.
126func Send[T any](id ID, sel SEL, args ...any) T {
127 var fn func(id ID, sel SEL, args ...any) T
128 var zero T
129 if runtime.GOARCH == "amd64" &&
130 reflect.ValueOf(zero).Kind() == reflect.Struct &&
131 reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize {
132 purego.RegisterFunc(&fn, objc_msgSend_stret_fn)
133 } else {
134 purego.RegisterFunc(&fn, objc_msgSend_fn)
135 }
136 return fn(id, sel, args...)
137}
138
139// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
140// as the receiver of a message. It specifies the class definition of the particular superclass that should
141// be messaged.
142type objc_super struct {
143 receiver ID
144 superClass Class
145}
146
147// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
148// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
149// of RegisterName.
150func (id ID) SendSuper(sel SEL, args ...any) ID {
151 super := &objc_super{
152 receiver: id,
153 superClass: id.Class(),
154 }
155 return objc_msgSendSuper2(super, sel, args...)
156}
157
158// SendSuper is a convenience method for sending message to object's super that can return any type.
159// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
160// It is best to cache the result of RegisterName.
161func SendSuper[T any](id ID, sel SEL, args ...any) T {
162 super := &objc_super{
163 receiver: id,
164 superClass: id.Class(),
165 }
166 var fn func(objcSuper *objc_super, sel SEL, args ...any) T
167 var zero T
168 if runtime.GOARCH == "amd64" &&
169 reflect.ValueOf(zero).Kind() == reflect.Struct &&
170 reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize {
171 purego.RegisterFunc(&fn, objc_msgSendSuper2_stret_fn)
172 } else {
173 purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
174 }
175 return fn(super, sel, args...)
176}
177
178// SEL is an opaque type that represents a method selector
179type SEL uintptr
180
181// RegisterName registers a method with the Objective-C runtime system, maps the method name to a selector,
182// and returns the selector value. This function grabs the global Objective-c lock. It is best the cache the
183// result of this function.
184func RegisterName(name string) SEL {
185 return sel_registerName(name)
186}
187
188// Class is an opaque type that represents an Objective-C class.
189type Class uintptr
190
191// GetClass returns the Class object for the named class, or nil if the class is not registered with the Objective-C runtime.
192func GetClass(name string) Class {
193 return objc_getClass(name)
194}
195
196// MethodDef represents the Go function and the selector that ObjC uses to access that function.
197type MethodDef struct {
198 Cmd SEL
199 Fn any
200}
201
202// IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically
203// generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes.
204// The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib.
205//
206// Take for example this Objective-C code:
207//
208// @property (readwrite) float value;
209//
210// In Go, the functions can be accessed as followed:
211//
212// var value = purego.Send[float32](id, purego.RegisterName("value"))
213// id.Send(purego.RegisterName("setValue:"), 3.46)
214//
215// [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html
216type IvarAttrib int
217
218const (
219 ReadOnly IvarAttrib = 1 << iota
220 ReadWrite
221)
222
223// FieldDef is a definition of a field to add to an Objective-C class.
224// The name of the field is what will be used to access it through the Ivar. If the type is bool
225// the name cannot start with `is` since a getter will be generated with the name `isBoolName`.
226// The name also cannot contain any spaces.
227// The type is the Go equivalent type of the Ivar.
228// Attribute determines if a getter and or setter method is generated for this field.
229type FieldDef struct {
230 Name string
231 Type reflect.Type
232 Attribute IvarAttrib
233}
234
235// ivarRegex checks to make sure the Ivar is correctly formatted
236var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*")
237
238// RegisterClass takes the name of the class to create, the superclass, a list of protocols this class
239// implements, a list of fields this class has and a list of methods. It returns the created class or an error
240// describing what went wrong.
241func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {
242 class := objc_allocateClassPair(superClass, name, 0)
243 if class == 0 {
244 return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
245 }
246 // Add Protocols
247 for _, p := range protocols {
248 if !class.AddProtocol(p) {
249 return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p))
250 }
251 }
252 // Add exported methods based on the selectors returned from ClassDef(string) SEL
253 for idx, def := range methods {
254 imp, err := func() (imp IMP, err error) {
255 defer func() {
256 if r := recover(); r != nil {
257 err = fmt.Errorf("objc: failed to create IMP: %s", r)
258 }
259 }()
260 return NewIMP(def.Fn), nil
261 }()
262 if err != nil {
263 return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
264 }
265 encoding, err := encodeFunc(def.Fn)
266 if err != nil {
267 return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
268 }
269 if !class.AddMethod(def.Cmd, imp, encoding) {
270 return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
271 }
272 }
273 // Add Ivars
274 for _, instVar := range ivars {
275 ivar := instVar
276 if !ivarRegex.MatchString(ivar.Name) {
277 return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name)
278 }
279 size := ivar.Type.Size()
280 alignment := uint8(math.Log2(float64(ivar.Type.Align())))
281 enc, err := encodeType(ivar.Type, false)
282 if err != nil {
283 return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
284 }
285 if !class_addIvar(class, ivar.Name, size, alignment, enc) {
286 return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
287 }
288 offset := class.InstanceVariable(ivar.Name).Offset()
289 switch ivar.Attribute {
290 case ReadWrite:
291 ty := reflect.FuncOf(
292 []reflect.Type{
293 reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type,
294 },
295 nil, false,
296 )
297 var encoding string
298 if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
299 return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
300 }
301 val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
302 // on entry the first and second arguments are ID and SEL followed by the value
303 if len(args) != 3 {
304 panic(fmt.Sprintf("objc: incorrect number of args. expected 3 got %d", len(args)))
305 }
306 // The following reflect code does the equivalent of this:
307 //
308 // ((*struct {
309 // Padding [offset]byte
310 // Value int
311 // })(unsafe.Pointer(args[0].Interface().(ID)))).v = 123
312 //
313 // However, since the type of the variable is unknown reflection is used to actually assign the value
314 id := args[0].Interface().(ID)
315 ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
316 reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem().Set(args[2])
317 return nil
318 }).Interface()
319 // this code only works for ascii but that shouldn't be a problem
320 selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00"
321 class.AddMethod(RegisterName(selector), NewIMP(val), encoding)
322 fallthrough // also implement the read method
323 case ReadOnly:
324 ty := reflect.FuncOf(
325 []reflect.Type{
326 reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)),
327 },
328 []reflect.Type{ivar.Type}, false,
329 )
330 var encoding string
331 if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
332 return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
333 }
334 val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
335 // on entry the first and second arguments are ID and SEL
336 if len(args) != 2 {
337 panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args)))
338 }
339 id := args[0].Interface().(ID)
340 ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
341 // the variable is located at an offset from the id
342 return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem()}
343 }).Interface()
344 if ivar.Type.Kind() == reflect.Bool {
345 // this code only works for ascii but that shouldn't be a problem
346 ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:]
347 }
348 class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding)
349 default:
350 return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute)
351 }
352 }
353 objc_registerClassPair(class)
354 return class, nil
355}
356
357const (
358 encId = "@"
359 encClass = "#"
360 encSelector = ":"
361 encChar = "c"
362 encUChar = "C"
363 encShort = "s"
364 encUShort = "S"
365 encInt = "i"
366 encUInt = "I"
367 encLong = "l"
368 encULong = "L"
369 encFloat = "f"
370 encDouble = "d"
371 encBool = "B"
372 encVoid = "v"
373 encPtr = "^"
374 encCharPtr = "*"
375 encStructBegin = "{"
376 encStructEnd = "}"
377 encUnsafePtr = "^v"
378)
379
380// encodeType returns a string representing a type as if it was given to @encode(typ)
381// Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
382func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
383 switch typ {
384 case reflect.TypeOf(Class(0)):
385 return encClass, nil
386 case reflect.TypeOf(ID(0)):
387 return encId, nil
388 case reflect.TypeOf(SEL(0)):
389 return encSelector, nil
390 }
391
392 kind := typ.Kind()
393 switch kind {
394 case reflect.Bool:
395 return encBool, nil
396 case reflect.Int:
397 return encLong, nil
398 case reflect.Int8:
399 return encChar, nil
400 case reflect.Int16:
401 return encShort, nil
402 case reflect.Int32:
403 return encInt, nil
404 case reflect.Int64:
405 return encULong, nil
406 case reflect.Uint:
407 return encULong, nil
408 case reflect.Uint8:
409 return encUChar, nil
410 case reflect.Uint16:
411 return encUShort, nil
412 case reflect.Uint32:
413 return encUInt, nil
414 case reflect.Uint64:
415 return encULong, nil
416 case reflect.Uintptr:
417 return encPtr, nil
418 case reflect.Float32:
419 return encFloat, nil
420 case reflect.Float64:
421 return encDouble, nil
422 case reflect.Ptr:
423 enc, err := encodeType(typ.Elem(), true)
424 return encPtr + enc, err
425 case reflect.Struct:
426 if insidePtr {
427 return encStructBegin + typ.Name() + encStructEnd, nil
428 }
429 encoding := encStructBegin
430 encoding += typ.Name()
431 encoding += "="
432 for i := 0; i < typ.NumField(); i++ {
433 f := typ.Field(i)
434 tmp, err := encodeType(f.Type, false)
435 if err != nil {
436 return "", err
437 }
438 encoding += tmp
439 }
440 encoding = encStructEnd
441 return encoding, nil
442 case reflect.UnsafePointer:
443 return encUnsafePtr, nil
444 case reflect.String:
445 return encCharPtr, nil
446 }
447
448 return "", errors.New(fmt.Sprintf("unhandled/invalid kind %v typed %v", kind, typ))
449}
450
451// encodeFunc returns a functions type as if it was given to @encode(fn)
452func encodeFunc(fn any) (string, error) {
453 typ := reflect.TypeOf(fn)
454 if typ.Kind() != reflect.Func {
455 return "", errors.New("not a func")
456 }
457
458 encoding := ""
459 switch typ.NumOut() {
460 case 0:
461 encoding += encVoid
462 case 1:
463 tmp, err := encodeType(typ.Out(0), false)
464 if err != nil {
465 return "", err
466 }
467 encoding += tmp
468 default:
469 return "", errors.New("too many output parameters")
470 }
471
472 if typ.NumIn() < 2 {
473 return "", errors.New("func doesn't take ID and SEL as its first two parameters")
474 }
475
476 encoding += encId
477
478 for i := 1; i < typ.NumIn(); i++ {
479 tmp, err := encodeType(typ.In(i), false)
480 if err != nil {
481 return "", err
482 }
483 encoding += tmp
484 }
485 return encoding, nil
486}
487
488// SuperClass returns the superclass of a class.
489// You should usually use NSObject‘s superclass method instead of this function.
490func (c Class) SuperClass() Class {
491 return class_getSuperclass(c)
492}
493
494// AddMethod adds a new method to a class with a given name and implementation.
495// The types argument is a string containing the mapping of parameters and return type.
496// Since the function must take at least two arguments—self and _cmd, the second and third
497// characters must be “@:” (the first character is the return type).
498func (c Class) AddMethod(name SEL, imp IMP, types string) bool {
499 return class_addMethod(c, name, imp, types)
500}
501
502// AddProtocol adds a protocol to a class.
503// Returns true if the protocol was added successfully, otherwise false (for example,
504// the class already conforms to that protocol).
505func (c Class) AddProtocol(protocol *Protocol) bool {
506 return class_addProtocol(c, protocol)
507}
508
509// InstanceSize returns the size in bytes of instances of the class or 0 if cls is nil
510func (c Class) InstanceSize() uintptr {
511 return class_getInstanceSize(c)
512}
513
514// InstanceVariable returns an Ivar data structure containing information about the instance variable specified by name.
515func (c Class) InstanceVariable(name string) Ivar {
516 return class_getInstanceVariable(c, name)
517}
518
519// Ivar an opaque type that represents an instance variable.
520type Ivar uintptr
521
522// Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value.
523//
524// For instance variables of type ID or other object types, call Ivar and SetIvar instead
525// of using this offset to access the instance variable data directly.
526func (i Ivar) Offset() uintptr {
527 return ivar_getOffset(i)
528}
529
530func (i Ivar) Name() string {
531 return ivar_getName(i)
532}
533
534// Protocol is a type that declares methods that can be implemented by any class.
535type Protocol [0]func()
536
537// GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
538func GetProtocol(name string) *Protocol {
539 return objc_getProtocol(name)
540}
541
542// Equals return true if the two protocols are the same.
543func (p *Protocol) Equals(p2 *Protocol) bool {
544 return protocol_isEqual(p, p2)
545}
546
547// IMP is a function pointer that can be called by Objective-C code.
548type IMP uintptr
549
550// NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
551// It returns an IMP function pointer that can be called by Objective-C code.
552// The function panics if an error occurs.
553// The function pointer is never deallocated.
554func NewIMP(fn any) IMP {
555 ty := reflect.TypeOf(fn)
556 if ty.Kind() != reflect.Func {
557 panic("objc: not a function")
558 }
559 // IMP is stricter than a normal callback
560 // id (*IMP)(id, SEL, ...)
561 switch {
562 case ty.NumIn() < 2:
563 fallthrough
564 case ty.In(0) != reflect.TypeOf(ID(0)):
565 fallthrough
566 case ty.In(1) != reflect.TypeOf(SEL(0)):
567 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
568 }
569 return IMP(purego.NewCallback(fn))
570}