v0.2.0 0c858ac

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 := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)
42	if err := purego.Dlerror(); err != "" {
43		panic("objc: " + err)
44	}
45	objc_msgSend_fn = purego.Dlsym(objc, "objc_msgSend")
46	purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn)
47	objc_msgSendSuper2_fn = purego.Dlsym(objc, "objc_msgSendSuper2")
48	purego.RegisterFunc(&objc_msgSendSuper2, objc_msgSendSuper2_fn)
49	purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
50	purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
51	purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
52	purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
53	purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
54	purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
55	purego.RegisterLibFunc(&class_getSuperclass, objc, "class_getSuperclass")
56	purego.RegisterLibFunc(&class_getInstanceVariable, objc, "class_getInstanceVariable")
57	purego.RegisterLibFunc(&class_addMethod, objc, "class_addMethod")
58	purego.RegisterLibFunc(&class_addIvar, objc, "class_addIvar")
59	purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
60	purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
61	purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
62}
63
64// ID is an opaque pointer to some Objective-C object
65type ID uintptr
66
67// Class returns the class of the object.
68func (id ID) Class() Class {
69	return object_getClass(id)
70}
71
72// Send is a convenience method for sending messages to objects. This function takes a SEL
73// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
74// of RegisterName.
75func (id ID) Send(sel SEL, args ...interface{}) ID {
76	return objc_msgSend(id, sel, args...)
77}
78
79// Send is a convenience method for sending messages to objects that can return any type.
80// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
81// It is best to cache the result of RegisterName.
82func Send[T any](id ID, sel SEL, args ...any) T {
83	var fn func(id ID, sel SEL, args ...any) T
84	purego.RegisterFunc(&fn, objc_msgSend_fn)
85	return fn(id, sel, args...)
86}
87
88// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
89// as the receiver of a message. It specifies the class definition of the particular superclass that should
90// be messaged.
91type objc_super struct {
92	receiver   ID
93	superClass Class
94}
95
96// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
97// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
98// of RegisterName.
99func (id ID) SendSuper(sel SEL, args ...interface{}) ID {
100	var super = &objc_super{
101		receiver:   id,
102		superClass: id.Class(),
103	}
104	return objc_msgSendSuper2(super, sel, args...)
105}
106
107// SendSuper is a convenience method for sending message to object's super 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 SendSuper[T any](id ID, sel SEL, args ...any) T {
111	var super = &objc_super{
112		receiver:   id,
113		superClass: id.Class(),
114	}
115	var fn func(objcSuper *objc_super, sel SEL, args ...any) T
116	purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
117	return fn(super, sel, args...)
118}
119
120// SEL is an opaque type that represents a method selector
121type SEL uintptr
122
123// RegisterName registers a method with the Objective-C runtime system, maps the method name to a selector,
124// and returns the selector value. This function grabs the global Objective-c lock. It is best the cache the
125// result of this function.
126func RegisterName(name string) SEL {
127	return sel_registerName(name)
128}
129
130// Class is an opaque type that represents an Objective-C class.
131type Class uintptr
132
133// GetClass returns the Class object for the named class, or nil if the class is not registered with the Objective-C runtime.
134func GetClass(name string) Class {
135	return objc_getClass(name)
136}
137
138// AllocateClassPair creates a new class and metaclass. Then returns the new class, or Nil if the class could not be created
139//
140// Deprecated: use RegisterClass instead
141func AllocateClassPair(super Class, name string, extraBytes uintptr) Class {
142	return objc_allocateClassPair(super, name, extraBytes)
143}
144
145// Selector is an interface that takes a Go method name
146// and returns the selector equivalent name.
147// If it returns a nil SEL then that method
148// is not added to the class object.
149type Selector interface {
150	Selector(string) SEL
151}
152
153// TagFormatError occurs when the parser fails to parse the objc tag in a Selector object
154var TagFormatError = errors.New(`objc tag doesn't match "ClassName : SuperClassName <Protocol, ...>""`)
155
156// MismatchError occurs when the Go struct definition doesn't match that of Objective-C
157var MismatchError = errors.New("go struct doesn't match objective-c struct")
158
159// RegisterClass takes a pointer to a struct that implements the Selector interface.
160// It will register the structs fields and pointer receiver methods in the Objective-C
161// runtime using the SEL returned from Selector. Any errors that occur trying to add
162// a Method or Ivar is returned as an error. Such errors may occur in parsing or because
163// the size of the struct does not match the size in Objective-C. If no errors occur
164// then the returned Class has been registered successfully.
165//
166// The struct's first field must be of type Class and have a tag that matches the format
167// `objc:"ClassName : SuperClassName <Protocol, ...>`. This tag is equal to how the class
168// would be defined in Objective-C.
169func RegisterClass(object Selector) (Class, error) {
170	ptr := reflect.TypeOf(object)
171	strct := ptr.Elem()
172	if strct.NumField() == 0 || strct.Field(0).Type != reflect.TypeOf(Class(0)) {
173		return 0, fmt.Errorf("objc: need objc.Class as first field: %w", MismatchError)
174	}
175	isa := strct.Field(0)
176	tag := isa.Tag.Get("objc")
177	if tag == "" {
178		return 0, fmt.Errorf("objc: missing objc tag: %w", TagFormatError)
179	}
180	// split contains the class name and super class name followed by all the Protocols
181	// start with two for ClassName : SuperClassName
182	var split = make([]string, 2)
183	{
184		// This is a simple parser for the objc tag that looks for the format
185		//  	"ClassName : SuperClassName <Protocol, ...>"
186		// It appends to the split variable with the [ClassName, SuperClassName, Protocol, ...]
187
188		var i int  // from tag[0:i] is whatever identifier is next
189		var r rune // r is the current rune
190		skipSpace := func() {
191			for _, c := range tag {
192				if c == ' ' {
193					tag = tag[1:]
194					continue
195				}
196				break
197			}
198		}
199		skipSpace()
200		// get ClassName
201		for i, r = range tag {
202			if r == ' ' || r == ':' {
203				break
204			}
205		}
206		split[0] = tag[0:i] // store ClassName
207		tag = tag[i:]
208
209		skipSpace()
210
211		// check for ':'
212		if len(tag) > 0 && tag[0] != ':' {
213			return 0, fmt.Errorf("objc: missing ':': %w", TagFormatError)
214		}
215		tag = tag[1:] // skip ':'
216		skipSpace()
217
218		// get SuperClassName
219		for i, r = range tag {
220			if r == ' ' {
221				break
222			} else if i+1 == len(tag) {
223				// if this is the last character in the string
224				// make sure to increment i so that tag[:i]
225				// includes the last character
226				i++
227				break
228			}
229		}
230		if len(tag) < i {
231			return 0, fmt.Errorf("objc: missing SuperClassName: %w", TagFormatError)
232		}
233		split[1] = tag[:i] // store SuperClassName
234		tag = tag[i:]      // drop SuperClassName
235		skipSpace()
236		if len(tag) > 0 {
237			if tag[0] != '<' {
238				return 0, fmt.Errorf("objc: expected '<': %w", TagFormatError)
239			}
240			tag = tag[1:] // drop '<'
241			// get Protocols
242		outer:
243			for {
244				skipSpace()
245				for i, r = range tag {
246					switch r {
247					case ' ':
248						split = append(split, tag[:i])
249						tag = tag[i:]
250						continue outer
251					case ',':
252						// If there is actually an identifier - add it.
253						if i > 0 {
254							split = append(split, tag[:i])
255							tag = tag[i:]
256						} else {
257							// Otherwise, drop ','
258							tag = tag[1:]
259						}
260						continue outer
261					case '>':
262						// If there is actually an identifier - add it.
263						if i > 0 {
264							split = append(split, tag[:i])
265							tag = tag[i:]
266						}
267						break outer
268					}
269				}
270				return 0, fmt.Errorf("objc: expected '>': %w", TagFormatError)
271			}
272		}
273	}
274	class := objc_allocateClassPair(GetClass(split[1]), split[0], 0)
275	if class == 0 {
276		return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0])
277	}
278	if len(split) > 2 {
279		// Add Protocols
280		for _, n := range split[2:] {
281			if !class.AddProtocol(GetProtocol(n)) {
282				return 0, fmt.Errorf("objc: couldn't add Protocol %s", n)
283			}
284		}
285	}
286	// Add exported methods based on the selectors returned from Selector(string) SEL
287	for i := 0; i < ptr.NumMethod(); i++ {
288		met := ptr.Method(i)
289		// we know this method is the interface one since RegisterClass
290		// requires that the struct implement Selector.
291		if met.Name == "Selector" {
292			continue
293		}
294		sel := object.Selector(met.Name)
295		if sel == 0 {
296			continue
297		}
298		fn := met.Func.Interface()
299		imp, err := func() (imp IMP, err error) {
300			defer func() {
301				if r := recover(); r != nil {
302					err = fmt.Errorf("objc: failed to create IMP: %s", r)
303				}
304			}()
305			return NewIMP(fn), nil
306		}()
307		if err != nil {
308			return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
309		}
310		encoding, err := encodeFunc(fn)
311		if err != nil {
312			return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err)
313		}
314		if !class.AddMethod(sel, imp, encoding) {
315			return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name)
316		}
317	}
318	// Add Ivars
319	// Start at 1 because we skip the class object which is first
320	for i := 1; i < strct.NumField(); i++ {
321		f := strct.Field(i)
322		if f.Name == "_" {
323			continue
324		}
325		size := f.Type.Size()
326		alignment := uint8(math.Log2(float64(f.Type.Align())))
327		enc, err := encodeType(f.Type, false)
328		if err != nil {
329			return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err)
330		}
331		if !class_addIvar(class, f.Name, size, alignment, enc) {
332			return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name)
333		}
334		if offset := class.InstanceVariable(f.Name).Offset(); offset != f.Offset {
335			return 0, fmt.Errorf("objc: couldn't add Ivar %s because offset (%d != %d)", f.Name, offset, f.Offset)
336		}
337	}
338	objc_registerClassPair(class)
339	if size1, size2 := class.InstanceSize(), strct.Size(); size1 != size2 {
340		return 0, fmt.Errorf("objc: sizes don't match %d != %d: %w", size1, size2, MismatchError)
341	}
342	return class, nil
343}
344
345const (
346	encId          = "@"
347	encClass       = "#"
348	encSelector    = ":"
349	encChar        = "c"
350	encUChar       = "C"
351	encShort       = "s"
352	encUShort      = "S"
353	encInt         = "i"
354	encUInt        = "I"
355	encLong        = "l"
356	encULong       = "L"
357	encFloat       = "f"
358	encDouble      = "d"
359	encBool        = "B"
360	encVoid        = "v"
361	encPtr         = "^"
362	encCharPtr     = "*"
363	encStructBegin = "{"
364	encStructEnd   = "}"
365	encUnsafePtr   = "^v"
366)
367
368// encodeType returns a string representing a type as if it was given to @encode(typ)
369// Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
370func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
371	switch typ {
372	case reflect.TypeOf(Class(0)):
373		return encClass, nil
374	case reflect.TypeOf(ID(0)):
375		return encId, nil
376	case reflect.TypeOf(SEL(0)):
377		return encSelector, nil
378	}
379
380	kind := typ.Kind()
381	switch kind {
382	case reflect.Bool:
383		return encBool, nil
384	case reflect.Int:
385		return encLong, nil
386	case reflect.Int8:
387		return encChar, nil
388	case reflect.Int16:
389		return encShort, nil
390	case reflect.Int32:
391		return encInt, nil
392	case reflect.Int64:
393		return encULong, nil
394	case reflect.Uint:
395		return encULong, nil
396	case reflect.Uint8:
397		return encUChar, nil
398	case reflect.Uint16:
399		return encUShort, nil
400	case reflect.Uint32:
401		return encUInt, nil
402	case reflect.Uint64:
403		return encULong, nil
404	case reflect.Uintptr:
405		return encPtr, nil
406	case reflect.Float32:
407		return encFloat, nil
408	case reflect.Float64:
409		return encDouble, nil
410	case reflect.Ptr:
411		enc, err := encodeType(typ.Elem(), true)
412		return encPtr + enc, err
413	case reflect.Struct:
414		if insidePtr {
415			return encStructBegin + typ.Name() + encStructEnd, nil
416		}
417		var encoding = encStructBegin
418		encoding += typ.Name()
419		encoding += "="
420		for i := 0; i < typ.NumField(); i++ {
421			f := typ.Field(i)
422			tmp, err := encodeType(f.Type, false)
423			if err != nil {
424				return "", err
425			}
426			encoding += tmp
427		}
428		encoding = encStructEnd
429		return encoding, nil
430	case reflect.UnsafePointer:
431		return encUnsafePtr, nil
432	case reflect.String:
433		return encCharPtr, nil
434	}
435
436	return "", errors.New(fmt.Sprintf("unhandled/invalid kind %v typed %v", kind, typ))
437}
438
439// encodeFunc returns a functions type as if it was given to @encode(fn)
440func encodeFunc(fn interface{}) (string, error) {
441	typ := reflect.TypeOf(fn)
442	if typ.Kind() != reflect.Func {
443		return "", errors.New("not a func")
444	}
445
446	encoding := ""
447	switch typ.NumOut() {
448	case 0:
449		encoding += encVoid
450	case 1:
451		tmp, err := encodeType(typ.Out(0), false)
452		if err != nil {
453			return "", err
454		}
455		encoding += tmp
456	default:
457		return "", errors.New("too many output parameters")
458	}
459
460	if typ.NumIn() < 2 {
461		return "", errors.New("func doesn't take ID and SEL as its first two parameters")
462	}
463
464	encoding += encId
465
466	for i := 1; i < typ.NumIn(); i++ {
467		tmp, err := encodeType(typ.In(i), false)
468		if err != nil {
469			return "", err
470		}
471		encoding += tmp
472	}
473	return encoding, nil
474}
475
476// SuperClass returns the superclass of a class.
477// You should usually use NSObject‘s superclass method instead of this function.
478func (c Class) SuperClass() Class {
479	return class_getSuperclass(c)
480}
481
482// AddMethod adds a new method to a class with a given name and implementation.
483// The types argument is a string containing the mapping of parameters and return type.
484// Since the function must take at least two arguments—self and _cmd, the second and third
485// characters must be “@:” (the first character is the return type).
486func (c Class) AddMethod(name SEL, imp IMP, types string) bool {
487	return class_addMethod(c, name, imp, types)
488}
489
490// AddIvar adds a new instance variable to a class.
491// It may only be called after AllocateClassPair and before Register.
492// Adding an instance variable to an existing class is not supported.
493// The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
494// It takes the instance of the type of the Ivar and a string representing the type.
495//
496// Deprecated: use RegisterClass instead
497func (c Class) AddIvar(name string, ty interface{}, types string) bool {
498	typeOf := reflect.TypeOf(ty)
499	size := typeOf.Size()
500	alignment := uint8(math.Log2(float64(typeOf.Align())))
501	return class_addIvar(c, name, size, alignment, types)
502}
503
504// AddProtocol adds a protocol to a class.
505// Returns true if the protocol was added successfully, otherwise false (for example,
506// the class already conforms to that protocol).
507func (c Class) AddProtocol(protocol *Protocol) bool {
508	return class_addProtocol(c, protocol)
509}
510
511// InstanceSize returns the size in bytes of instances of the class or 0 if cls is nil
512func (c Class) InstanceSize() uintptr {
513	return class_getInstanceSize(c)
514}
515
516// InstanceVariable returns an Ivar data structure containing information about the instance variable specified by name.
517func (c Class) InstanceVariable(name string) Ivar {
518	return class_getInstanceVariable(c, name)
519}
520
521// Register registers a class that was allocated using AllocateClassPair.
522// It can now be used to make objects by sending it either alloc and init or new.
523//
524// Deprecated: use RegisterClass instead
525func (c Class) Register() {
526	objc_registerClassPair(c)
527}
528
529// Ivar an opaque type that represents an instance variable.
530type Ivar uintptr
531
532// Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value.
533//
534// For instance variables of type ID or other object types, call Ivar and SetIvar instead
535// of using this offset to access the instance variable data directly.
536func (i Ivar) Offset() uintptr {
537	return ivar_getOffset(i)
538}
539
540// Protocol is a type that declares methods that can be implemented by any class.
541type Protocol uintptr
542
543// GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
544func GetProtocol(name string) *Protocol {
545	return objc_getProtocol(name)
546}
547
548// IMP is a function pointer that can be called by Objective-C code.
549type IMP uintptr
550
551// NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
552// ID may instead be a pointer to a struct whose first field has type Class.
553// It returns an IMP function pointer that can be called by Objective-C code.
554// The function pointer is never deallocated.
555func NewIMP(fn interface{}) IMP {
556	ty := reflect.TypeOf(fn)
557	if ty.Kind() != reflect.Func {
558		panic("objc: not a function")
559	}
560	// IMP is stricter than a normal callback
561	// id (*IMP)(id, SEL, ...)
562	switch {
563	case ty.NumIn() < 2:
564		fallthrough
565	case ty.In(0).Kind() != reflect.Uintptr && // checks if it's objc.ID
566		// or that it's a pointer to a struct
567		(ty.In(0).Kind() != reflect.Pointer || ty.In(0).Elem().Kind() != reflect.Struct ||
568			// and that the structs first field is an objc.Class
569			ty.In(0).Elem().Field(0).Type != reflect.TypeOf(Class(0))):
570		fallthrough
571	case ty.In(1).Kind() != reflect.Uintptr:
572		panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
573	}
574	return IMP(purego.NewCallback(fn))
575}

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