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 "fmt"
10 "math"
11 "reflect"
12 "runtime"
13 "unsafe"
14
15 "github.com/ebitengine/purego"
16 "github.com/ebitengine/purego/internal/strings"
17)
18
19//TODO: support try/catch?
20//https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
21
22var (
23 objc = purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)
24
25 objc_msgSend = purego.Dlsym(objc, "objc_msgSend")
26 objc_msgSendSuper2 = purego.Dlsym(objc, "objc_msgSendSuper2")
27 objc_getClass = purego.Dlsym(objc, "objc_getClass")
28 objc_allocateClassPair = purego.Dlsym(objc, "objc_allocateClassPair")
29 objc_registerClassPair = purego.Dlsym(objc, "objc_registerClassPair")
30 sel_registerName = purego.Dlsym(objc, "sel_registerName")
31 class_getSuperclass = purego.Dlsym(objc, "class_getSuperclass")
32 class_getInstanceVariable = purego.Dlsym(objc, "class_getInstanceVariable")
33 class_addMethod = purego.Dlsym(objc, "class_addMethod")
34 class_addIvar = purego.Dlsym(objc, "class_addIvar")
35 ivar_getOffset = purego.Dlsym(objc, "ivar_getOffset")
36 object_getClass = purego.Dlsym(objc, "object_getClass")
37)
38
39// ID is an opaque pointer to some Objective-C object
40type ID uintptr
41
42// Class returns the class of the object.
43func (id ID) Class() Class {
44 ret, _, _ := purego.SyscallN(object_getClass, uintptr(id))
45 return Class(ret)
46}
47
48// Send is a convenience method for sending messages to objects.
49func (id ID) Send(sel SEL, args ...interface{}) ID {
50 tmp, keepAlive := createArgs(id, sel, args...)
51 defer keepAlive()
52 ret, _, _ := purego.SyscallN(objc_msgSend, tmp...)
53 return ID(ret)
54}
55
56// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
57// as the receiver of a message. It specifies the class definition of the particular superclass that should
58// be messaged.
59type objc_super struct {
60 receiver ID
61 superClass Class
62}
63
64// SendSuper is a convenience method for sending message to object's super
65func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
66 var super = &objc_super{
67 receiver: id,
68 superClass: id.Class(),
69 }
70 tmp, keepAlive := createArgs(0, sel, args...)
71 defer keepAlive()
72 tmp[0] = uintptr(unsafe.Pointer(super)) // if createArgs splits the stack the pointer would be wrong
73 ret, _, _ := purego.SyscallN(objc_msgSendSuper2, tmp...)
74 return ID(ret)
75}
76
77func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr, keepAlive func()) {
78 out = make([]uintptr, 2, len(args)+2)
79 out[0] = uintptr(cls)
80 out[1] = uintptr(sel)
81 var alive []interface{}
82 for _, a := range args {
83 switch v := a.(type) {
84 case ID:
85 out = append(out, uintptr(v))
86 case Class:
87 out = append(out, uintptr(v))
88 case SEL:
89 out = append(out, uintptr(v))
90 case IMP:
91 out = append(out, uintptr(v))
92 case bool:
93 if v {
94 out = append(out, uintptr(1))
95 } else {
96 out = append(out, uintptr(0))
97 }
98 case string:
99 c := strings.CString(v)
100 alive = append(alive, c)
101 // NOTE: that although the pointer will be kept alive
102 // if there are any stack splits before this uintptr
103 // is no longer needed the pointer will point to random memory.
104 out = append(out, uintptr(unsafe.Pointer(c)))
105 case unsafe.Pointer:
106 alive = append(alive, v)
107 // NOTE: that although the pointer will be kept alive
108 // if there are any stack splits before this uintptr
109 // is no longer needed the pointer will point to random memory.
110 out = append(out, uintptr(v))
111 case uintptr:
112 out = append(out, v)
113 case int:
114 out = append(out, uintptr(v))
115 case uint:
116 out = append(out, uintptr(v))
117 default:
118 panic(fmt.Sprintf("objc: unknown type %T", v))
119 }
120 }
121 return out, func() {
122 runtime.KeepAlive(alive)
123 }
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.
131func RegisterName(name string) SEL {
132 n := strings.CString(name)
133 ret, _, _ := purego.SyscallN(sel_registerName, uintptr(unsafe.Pointer(n)))
134 runtime.KeepAlive(n)
135 return SEL(ret)
136}
137
138// Class is an opaque type that represents an Objective-C class.
139type Class uintptr
140
141// GetClass returns the Class object for the named class, or nil if the class is not registered with the Objective-C runtime.
142func GetClass(name string) Class {
143 n := strings.CString(name)
144 ret, _, _ := purego.SyscallN(objc_getClass, uintptr(unsafe.Pointer(n)))
145 runtime.KeepAlive(n)
146 return Class(ret)
147}
148
149// AllocateClassPair creates a new class and metaclass. Then returns the new class, or Nil if the class could not be created
150func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
151 n := strings.CString(name)
152 ret, _, _ := purego.SyscallN(objc_allocateClassPair, uintptr(super), uintptr(unsafe.Pointer(n)), extraBytes)
153 runtime.KeepAlive(n)
154 return Class(ret)
155}
156
157// SuperClass returns the superclass of a class.
158// You should usually use NSObject‘s superclass method instead of this function.
159func (c Class) SuperClass() Class {
160 ret, _, _ := purego.SyscallN(class_getSuperclass, uintptr(c))
161 return Class(ret)
162}
163
164// AddMethod adds a new method to a class with a given name and implementation.
165// The types argument is a string containing the mapping of parameters and return type.
166// Since the function must take at least two arguments—self and _cmd, the second and third
167// characters must be “@:” (the first character is the return type).
168func (c Class) AddMethod(name SEL, imp IMP, types string) bool {
169 t := strings.CString(types)
170 ret, _, _ := purego.SyscallN(class_addMethod, uintptr(c), uintptr(name), uintptr(imp), uintptr(unsafe.Pointer(t)))
171 runtime.KeepAlive(t)
172 return byte(ret) != 0
173}
174
175// AddIvar adds a new instance variable to a class.
176// It may only be called after AllocateClassPair and before Register.
177// Adding an instance variable to an existing class is not supported.
178// The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
179// It takes the instance of the type of the Ivar and a string representing the type.
180func (c Class) AddIvar(name string, ty interface{}, types string) bool {
181 n := strings.CString(name)
182 t := strings.CString(types)
183 typeOf := reflect.TypeOf(ty)
184 size := typeOf.Size()
185 alignment := uint8(math.Log2(float64(typeOf.Align())))
186 ret, _, _ := purego.SyscallN(class_addIvar, uintptr(c), uintptr(unsafe.Pointer(n)), size, uintptr(alignment), uintptr(unsafe.Pointer(t)))
187 runtime.KeepAlive(n)
188 runtime.KeepAlive(t)
189 return byte(ret) != 0
190}
191
192// InstanceVariable returns an Ivar data structure containing information about the instance variable specified by name.
193func (c Class) InstanceVariable(name string) Ivar {
194 n := strings.CString(name)
195 ret, _, _ := purego.SyscallN(class_getInstanceVariable, uintptr(c), uintptr(unsafe.Pointer(n)))
196 runtime.KeepAlive(n)
197 return Ivar(ret)
198}
199
200// Register registers a class that was allocated using AllocateClassPair.
201// It can now be used to make objects by sending it either alloc and init or new.
202func (c Class) Register() {
203 purego.SyscallN(objc_registerClassPair, uintptr(c))
204}
205
206// Ivar an opaque type that represents an instance variable.
207type Ivar uintptr
208
209// Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value.
210//
211// For instance variables of type ID or other object types, call Ivar and SetIvar instead
212// of using this offset to access the instance variable data directly.
213func (i Ivar) Offset() uintptr {
214 ret, _, _ := purego.SyscallN(ivar_getOffset, uintptr(i))
215 return ret
216}
217
218// IMP is a function pointer that can be called by Objective-C code.
219type IMP uintptr
220
221// NewIMP takes a Go function that takes (ID, SEL) as its first two arguments. It returns an IMP function
222// pointer that can be called by Objective-C code. The function pointer is never deallocated.
223func NewIMP(fn interface{}) IMP {
224 ty := reflect.TypeOf(fn)
225 if ty.Kind() != reflect.Func {
226 panic("objc: not a function")
227 }
228 // IMP is stricter than a normal callback
229 // id (*IMP)(id, SEL, ...)
230 switch {
231 case ty.NumIn() < 2:
232 fallthrough
233 case ty.In(0).Kind() != reflect.Uintptr:
234 fallthrough
235 case ty.In(1).Kind() != reflect.Uintptr:
236 panic("objc: NewIMP must take a (id, SEL) as its first two arguments")
237 }
238 return IMP(purego.NewCallback(fn))
239}