v0.6.0 32fe07e

objc_runtime_darwin.go RawHistoryFolder

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	"unicode"
15	"unsafe"
16
17	"github.com/ebitengine/purego"
18)
19
20// TODO: support try/catch?
21// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
22var (
23	objc_msgSend_fn           uintptr
24	objc_msgSend              func(obj ID, cmd SEL, args ...interface{}) ID
25	objc_msgSendSuper2_fn     uintptr
26	objc_msgSendSuper2        func(super *objc_super, cmd SEL, args ...interface{}) ID
27	objc_getClass             func(name string) Class
28	objc_getProtocol          func(name string) *Protocol
29	objc_allocateClassPair    func(super Class, name string, extraBytes uintptr) Class
30	objc_registerClassPair    func(class Class)
31	sel_registerName          func(name string) SEL
32	class_getSuperclass       func(class Class) Class
33	class_getInstanceVariable func(class Class, name string) Ivar
34	class_getInstanceSize     func(class Class) uintptr
35	class_addMethod           func(class Class, name SEL, imp IMP, types string) bool
36	class_addIvar             func(class Class, name string, size uintptr, alignment uint8, types string) bool
37	class_addProtocol         func(class Class, protocol *Protocol) bool
38	ivar_getOffset            func(ivar Ivar) uintptr
39	ivar_getName              func(ivar Ivar) string
40	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
45)
46
47func init() {
48	objc, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)
49	if err != nil {
50		panic(fmt.Errorf("objc: %w", err))
51	}
52	objc_msgSend_fn, err = purego.Dlsym(objc, "objc_msgSend")
53	if err != nil {
54		panic(fmt.Errorf("objc: %w", err))
55	}
56	purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn)
57	objc_msgSendSuper2_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2")
58	if err != nil {
59		panic(fmt.Errorf("objc: %w", err))
60	}
61	purego.RegisterFunc(&objc_msgSendSuper2, objc_msgSendSuper2_fn)
62	purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
63	purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
64	purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
65	purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
66	purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
67	purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
68	purego.RegisterLibFunc(&class_getSuperclass, objc, "class_getSuperclass")
69	purego.RegisterLibFunc(&class_getInstanceVariable, objc, "class_getInstanceVariable")
70	purego.RegisterLibFunc(&class_addMethod, objc, "class_addMethod")
71	purego.RegisterLibFunc(&class_addIvar, objc, "class_addIvar")
72	purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
73	purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
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")
80}
81
82// ID is an opaque pointer to some Objective-C object
83type ID uintptr
84
85// Class returns the class of the object.
86func (id ID) Class() Class {
87	return object_getClass(id)
88}
89
90// Send is a convenience method for sending messages to objects. This function takes a SEL
91// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
92// of RegisterName.
93func (id ID) Send(sel SEL, args ...interface{}) ID {
94	return objc_msgSend(id, sel, args...)
95}
96
97// GetIvar reads the value of an instance variable in an object.
98func (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.
103func (id ID) SetIvar(ivar Ivar, value ID) {
104	object_setIvar(id, ivar, value)
105}
106
107// Send is a convenience method for sending messages to objects 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 Send[T any](id ID, sel SEL, args ...any) T {
111	var fn func(id ID, sel SEL, args ...any) T
112	purego.RegisterFunc(&fn, objc_msgSend_fn)
113	return fn(id, sel, args...)
114}
115
116// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
117// as the receiver of a message. It specifies the class definition of the particular superclass that should
118// be messaged.
119type objc_super struct {
120	receiver   ID
121	superClass Class
122}
123
124// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
125// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
126// of RegisterName.
127func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
128	super := &objc_super{
129		receiver:   id,
130		superClass: id.Class(),
131	}
132	return objc_msgSendSuper2(super, sel, args...)
133}
134
135// SendSuper is a convenience method for sending message to object's super that can return any type.
136// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
137// It is best to cache the result of RegisterName.
138func SendSuper[T any](id ID, sel SEL, args ...any) T {
139	super := &objc_super{
140		receiver:   id,
141		superClass: id.Class(),
142	}
143	var fn func(objcSuper *objc_super, sel SEL, args ...any) T
144	purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
145	return fn(super, sel, args...)
146}
147
148// SEL is an opaque type that represents a method selector
149type SEL uintptr
150
151// RegisterName registers a method with the Objective-C runtime system, maps the method name to a selector,
152// and returns the selector value. This function grabs the global Objective-c lock. It is best the cache the
153// result of this function.
154func RegisterName(name string) SEL {
155	return sel_registerName(name)
156}
157
158// Class is an opaque type that represents an Objective-C class.
159type Class uintptr
160
161// GetClass returns the Class object for the named class, or nil if the class is not registered with the Objective-C runtime.
162func GetClass(name string) Class {
163	return objc_getClass(name)
164}
165
166// AllocateClassPair creates a new class and metaclass. Then returns the new class, or Nil if the class could not be created
167//
168// Deprecated: use RegisterClass instead
169func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
170	return objc_allocateClassPair(super, name, extraBytes)
171}
172
173// MethodDef represents the Go function and the selector that ObjC uses to access that function.
174type MethodDef struct {
175	Cmd SEL
176	Fn  any
177}
178
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
193type IvarAttrib int
194
195const (
196	ReadOnly IvarAttrib = 1 << iota
197	ReadWrite
198)
199
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.
206type FieldDef struct {
207	Name      string
208	Type      reflect.Type
209	Attribute IvarAttrib
210}
211
212// ivarRegex checks to make sure the Ivar is correctly formatted
213var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*")
214
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.
218func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {
219	class := objc_allocateClassPair(superClass, name, 0)
220	if class == 0 {
221		return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
222	}
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))
227		}
228	}
229	// Add exported methods based on the selectors returned from ClassDef(string) SEL
230	for idx, def := range methods {
231		imp, err := func() (imp IMP, err error) {
232			defer func() {
233				if r := recover(); r != nil {
234					err = fmt.Errorf("objc: failed to create IMP: %s", r)
235				}
236			}()
237			return NewIMP(def.Fn), nil
238		}()
239		if err != nil {
240			return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
241		}
242		encoding, err := encodeFunc(def.Fn)
243		if err != nil {
244			return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
245		}
246		if !class.AddMethod(def.Cmd, imp, encoding) {
247			return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
248		}
249	}
250	// Add Ivars
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)
255		}
256		size := ivar.Type.Size()
257		alignment := uint8(math.Log2(float64(ivar.Type.Align())))
258		enc, err := encodeType(ivar.Type, false)
259		if err != nil {
260			return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
261		}
262		if !class_addIvar(class, ivar.Name, size, alignment, enc) {
263			return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
264		}
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)
328		}
329	}
330	objc_registerClassPair(class)
331	return class, nil
332}
333
334const (
335	encId          = "@"
336	encClass       = "#"
337	encSelector    = ":"
338	encChar        = "c"
339	encUChar       = "C"
340	encShort       = "s"
341	encUShort      = "S"
342	encInt         = "i"
343	encUInt        = "I"
344	encLong        = "l"
345	encULong       = "L"
346	encFloat       = "f"
347	encDouble      = "d"
348	encBool        = "B"
349	encVoid        = "v"
350	encPtr         = "^"
351	encCharPtr     = "*"
352	encStructBegin = "{"
353	encStructEnd   = "}"
354	encUnsafePtr   = "^v"
355)
356
357// encodeType returns a string representing a type as if it was given to @encode(typ)
358// Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
359func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
360	switch typ {
361	case reflect.TypeOf(Class(0)):
362		return encClass, nil
363	case reflect.TypeOf(ID(0)):
364		return encId, nil
365	case reflect.TypeOf(SEL(0)):
366		return encSelector, nil
367	}
368
369	kind := typ.Kind()
370	switch kind {
371	case reflect.Bool:
372		return encBool, nil
373	case reflect.Int:
374		return encLong, nil
375	case reflect.Int8:
376		return encChar, nil
377	case reflect.Int16:
378		return encShort, nil
379	case reflect.Int32:
380		return encInt, nil
381	case reflect.Int64:
382		return encULong, nil
383	case reflect.Uint:
384		return encULong, nil
385	case reflect.Uint8:
386		return encUChar, nil
387	case reflect.Uint16:
388		return encUShort, nil
389	case reflect.Uint32:
390		return encUInt, nil
391	case reflect.Uint64:
392		return encULong, nil
393	case reflect.Uintptr:
394		return encPtr, nil
395	case reflect.Float32:
396		return encFloat, nil
397	case reflect.Float64:
398		return encDouble, nil
399	case reflect.Ptr:
400		enc, err := encodeType(typ.Elem(), true)
401		return encPtr + enc, err
402	case reflect.Struct:
403		if insidePtr {
404			return encStructBegin + typ.Name() + encStructEnd, nil
405		}
406		encoding := encStructBegin
407		encoding += typ.Name()
408		encoding += "="
409		for i := 0; i < typ.NumField(); i++ {
410			f := typ.Field(i)
411			tmp, err := encodeType(f.Type, false)
412			if err != nil {
413				return "", err
414			}
415			encoding += tmp
416		}
417		encoding = encStructEnd
418		return encoding, nil
419	case reflect.UnsafePointer:
420		return encUnsafePtr, nil
421	case reflect.String:
422		return encCharPtr, nil
423	}
424
425	return "", errors.New(fmt.Sprintf("unhandled/invalid kind %v typed %v", kind, typ))
426}
427
428// encodeFunc returns a functions type as if it was given to @encode(fn)
429func encodeFunc(fn interface{}) (string, error) {
430	typ := reflect.TypeOf(fn)
431	if typ.Kind() != reflect.Func {
432		return "", errors.New("not a func")
433	}
434
435	encoding := ""
436	switch typ.NumOut() {
437	case 0:
438		encoding += encVoid
439	case 1:
440		tmp, err := encodeType(typ.Out(0), false)
441		if err != nil {
442			return "", err
443		}
444		encoding += tmp
445	default:
446		return "", errors.New("too many output parameters")
447	}
448
449	if typ.NumIn() < 2 {
450		return "", errors.New("func doesn't take ID and SEL as its first two parameters")
451	}
452
453	encoding += encId
454
455	for i := 1; i < typ.NumIn(); i++ {
456		tmp, err := encodeType(typ.In(i), false)
457		if err != nil {
458			return "", err
459		}
460		encoding += tmp
461	}
462	return encoding, nil
463}
464
465// SuperClass returns the superclass of a class.
466// You should usually use NSObject‘s superclass method instead of this function.
467func (c Class) SuperClass() Class {
468	return class_getSuperclass(c)
469}
470
471// AddMethod adds a new method to a class with a given name and implementation.
472// The types argument is a string containing the mapping of parameters and return type.
473// Since the function must take at least two arguments—self and _cmd, the second and third
474// characters must be “@:” (the first character is the return type).
475func (c Class) AddMethod(name SEL, imp IMP, types string) bool {
476	return class_addMethod(c, name, imp, types)
477}
478
479// AddIvar adds a new instance variable to a class.
480// It may only be called after AllocateClassPair and before Register.
481// Adding an instance variable to an existing class is not supported.
482// The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
483// It takes the instance of the type of the Ivar and a string representing the type.
484//
485// Deprecated: use RegisterClass instead
486func (c Class) AddIvar(name string, ty interface{}, types string) bool {
487	typeOf := reflect.TypeOf(ty)
488	size := typeOf.Size()
489	alignment := uint8(math.Log2(float64(typeOf.Align())))
490	return class_addIvar(c, name, size, alignment, types)
491}
492
493// AddProtocol adds a protocol to a class.
494// Returns true if the protocol was added successfully, otherwise false (for example,
495// the class already conforms to that protocol).
496func (c Class) AddProtocol(protocol *Protocol) bool {
497	return class_addProtocol(c, protocol)
498}
499
500// InstanceSize returns the size in bytes of instances of the class or 0 if cls is nil
501func (c Class) InstanceSize() uintptr {
502	return class_getInstanceSize(c)
503}
504
505// InstanceVariable returns an Ivar data structure containing information about the instance variable specified by name.
506func (c Class) InstanceVariable(name string) Ivar {
507	return class_getInstanceVariable(c, name)
508}
509
510// Register registers a class that was allocated using AllocateClassPair.
511// It can now be used to make objects by sending it either alloc and init or new.
512//
513// Deprecated: use RegisterClass instead
514func (c Class) Register() {
515	objc_registerClassPair(c)
516}
517
518// Ivar an opaque type that represents an instance variable.
519type Ivar uintptr
520
521// Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value.
522//
523// For instance variables of type ID or other object types, call Ivar and SetIvar instead
524// of using this offset to access the instance variable data directly.
525func (i Ivar) Offset() uintptr {
526	return ivar_getOffset(i)
527}
528
529func (i Ivar) Name() string {
530	return ivar_getName(i)
531}
532
533// Protocol is a type that declares methods that can be implemented by any class.
534type Protocol [0]func()
535
536// GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
537func GetProtocol(name string) *Protocol {
538	return objc_getProtocol(name)
539}
540
541// Equals return true if the two protocols are the same.
542func (p *Protocol) Equals(p2 *Protocol) bool {
543	return protocol_isEqual(p, p2)
544}
545
546// IMP is a function pointer that can be called by Objective-C code.
547type IMP uintptr
548
549// NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
550// It returns an IMP function pointer that can be called by Objective-C code.
551// The function panics if an error occurs.
552// The function pointer is never deallocated.
553func NewIMP(fn interface{}) IMP {
554	ty := reflect.TypeOf(fn)
555	if ty.Kind() != reflect.Func {
556		panic("objc: not a function")
557	}
558	// IMP is stricter than a normal callback
559	// id (*IMP)(id, SEL, ...)
560	switch {
561	case ty.NumIn() < 2:
562		fallthrough
563	case ty.In(0) != reflect.TypeOf(ID(0)):
564		fallthrough
565	case ty.In(1) != reflect.TypeOf(SEL(0)):
566		panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
567	}
568	return IMP(purego.NewCallback(fn))
569}

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