v0.4.0-alpha 30d39b1

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
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}

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