v0.12.0-alpha 7e3b362

objc_block_darwin.go RawHistoryFolder

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3
4package objc
5
6import (
7	"fmt"
8	"reflect"
9	"structs"
10	"sync"
11	"unsafe"
12
13	"github.com/ebitengine/purego"
14)
15
16const (
17	// The end-goal of these defaults is to get an Objective-C memory-managed block object
18	// that won't try to free() a Go pointer, but will call our custom blockFunctionCache.Delete()
19	// when the reference count drops to zero, so the associated function is also unreferenced.
20
21	// blockBaseClass is the name of the class that block objects will be initialized with.
22	blockBaseClass = "__NSMallocBlock__"
23	// blockFlags is the set of flags that block objects will be initialized with.
24	blockFlags = blockHasCopyDispose | blockHasSignature
25
26	// blockHasCopyDispose is a flag that tells the Objective-C runtime the block exports Copy and/or Dispose helpers.
27	blockHasCopyDispose = 1 << 25
28
29	// blockHasSignature is a flag that tells the Objective-C runtime the block exports a function signature.
30	blockHasSignature = 1 << 30
31)
32
33// blockDescriptor is the Go representation of an Objective-C block descriptor.
34// It is a component to be referenced by blockDescriptor.
35//
36// The layout of this struct matches Block_literal_1 described in https://clang.llvm.org/docs/Block-ABI-Apple.html#high-level
37type blockDescriptor struct {
38	_         structs.HostLayout
39	_         uintptr
40	size      uintptr
41	_         uintptr
42	dispose   uintptr
43	signature *uint8
44}
45
46// blockLayout is the Go representation of the structure abstracted by a block pointer.
47// From the Objective-C point of view, a pointer to this struct is equivalent to an ID that
48// references a block.
49//
50// The layout of this struct matches __block_literal_1 described in https://clang.llvm.org/docs/Block-ABI-Apple.html#high-level
51type blockLayout struct {
52	_          structs.HostLayout
53	isa        Class
54	flags      uint32
55	_          uint32
56	invoke     uintptr
57	descriptor *blockDescriptor
58}
59
60// blockFunctionCache is a thread safe cache of block layouts.
61//
62// The function closures themselves are kept alive by caching them internally until the Objective-C runtime indicates that
63// they can be released (presumably when the reference count reaches zero). This approach is used instead of appending the function
64// object to the block allocation, where it is out of the visible domain of Go's GC.
65type blockFunctionCache struct {
66	mutex     sync.RWMutex
67	functions map[Block]reflect.Value
68}
69
70// Load retrieves a function (in the form of a reflect.Value, so Call can be invoked) associated with the key Block.
71func (b *blockFunctionCache) Load(key Block) reflect.Value {
72	b.mutex.RLock()
73	defer b.mutex.RUnlock()
74	return b.functions[key]
75}
76
77// Store associates a function (in the form of a reflect.Value) with the key Block.
78func (b *blockFunctionCache) Store(key Block, value reflect.Value) Block {
79	b.mutex.Lock()
80	defer b.mutex.Unlock()
81	b.functions[key] = value
82	return key
83}
84
85// Delete removed the function associated with the key Block.
86func (b *blockFunctionCache) Delete(key Block) {
87	b.mutex.Lock()
88	defer b.mutex.Unlock()
89	delete(b.functions, key)
90}
91
92// newBlockFunctionCache initializes a new blockFunctionCache
93func newBlockFunctionCache() *blockFunctionCache {
94	return &blockFunctionCache{functions: map[Block]reflect.Value{}}
95}
96
97// blockCache is a thread safe cache of block layouts.
98//
99// It takes advantage of the block being the first argument of a block call being the block closure,
100// only invoking [github.com/ebitengine/purego.NewCallback] when it encounters a new function type (rather than on for every block creation).
101// This should mitigate block creations putting pressure on the callback limit.
102type blockCache struct {
103	sync.Mutex
104	descriptorTemplate blockDescriptor
105	layoutTemplate     blockLayout
106	layouts            map[reflect.Type]blockLayout
107	Functions          *blockFunctionCache
108}
109
110// encode returns a blocks type as if it was given to @encode(typ)
111func (*blockCache) encode(typ reflect.Type) *uint8 {
112	// this algorithm was copied from encodeFunc,
113	// but altered to panic on error, and to only accept a block-type signature.
114	if typ == nil || typ.Kind() != reflect.Func {
115		panic("objc: not a function")
116	}
117
118	var encoding string
119	switch typ.NumOut() {
120	case 0:
121		encoding = encVoid
122	default:
123		returnType, err := encodeType(typ.Out(0), false)
124		if err != nil {
125			panic(fmt.Sprintf("objc: %v", err))
126		}
127		encoding = returnType
128	}
129
130	if typ.NumIn() == 0 || typ.In(0) != reflect.TypeFor[Block]() {
131		panic(fmt.Sprintf("objc: A Block implementation must take a Block as its first argument; got %v", typ.String()))
132	}
133
134	encoding += encId
135	for i := 1; i < typ.NumIn(); i++ {
136		argType, err := encodeType(typ.In(i), false)
137		if err != nil {
138			panic(fmt.Sprintf("objc: %v", err))
139		}
140		encoding = fmt.Sprint(encoding, argType)
141	}
142
143	// return the encoding as a C-style string.
144	return &append([]uint8(encoding), 0)[0]
145}
146
147// getLayout retrieves a blockLayout VALUE constructed with the supplied function type.
148// It will panic if the type is not a valid block function.
149func (b *blockCache) getLayout(typ reflect.Type) blockLayout {
150	b.Lock()
151	defer b.Unlock()
152
153	// return the cached layout, if it exists.
154	if layout, ok := b.layouts[typ]; ok {
155		return layout
156	}
157
158	// otherwise: create a layout, and populate it with the default templates
159	layout := b.layoutTemplate
160	layout.descriptor = &blockDescriptor{}
161	*layout.descriptor = b.descriptorTemplate
162
163	// getting the signature now will panic on invalid types before we invest in creating a callback.
164	layout.descriptor.signature = b.encode(typ)
165
166	// create a global callback.
167	// this single callback can dispatch to any function with the same signature,
168	// since the user-provided functions are associated with the actual block allocations.
169	layout.invoke = purego.NewCallback(
170		reflect.MakeFunc(
171			typ,
172			func(args []reflect.Value) (results []reflect.Value) {
173				block, ok := reflect.TypeAssert[Block](args[0])
174				if !ok {
175					panic(fmt.Sprintf("objc: block argument is not a block but %s", args[0].Type().String()))
176				}
177				return b.Functions.Load(block).Call(args)
178			},
179		).Interface(),
180	)
181
182	// store it and return it
183	b.layouts[typ] = layout
184	return layout
185}
186
187// newBlockCache initializes a block cache.
188// It should not be called until AFTER libobjc is fully initialized.
189func newBlockCache() *blockCache {
190	cache := &blockCache{
191		descriptorTemplate: blockDescriptor{
192			size: unsafe.Sizeof(blockLayout{}),
193		},
194		layoutTemplate: blockLayout{
195			isa:   GetClass(blockBaseClass),
196			flags: blockFlags,
197		},
198		layouts:   map[reflect.Type]blockLayout{},
199		Functions: newBlockFunctionCache(),
200	}
201	cache.descriptorTemplate.dispose = purego.NewCallback(cache.Functions.Delete)
202	return cache
203}
204
205// theBlocksCache is the global block cache
206var theBlocksCache *blockCache
207
208// Block is an opaque pointer to an Objective-C object containing a function with its associated closure.
209type Block ID
210
211// Copy creates a copy of a block on the Objective-C heap (or increments the reference count if already on the heap).
212// Use [Block.Release] to free the copy when it is no longer in use.
213func (b Block) Copy() Block {
214	return _Block_copy(b)
215}
216
217// Invoke calls the implementation of a block.
218func (b Block) Invoke(args ...any) {
219	fn := theBlocksCache.Functions.Load(b)
220
221	reflectedArgs := make([]reflect.Value, len(args)+1)
222	reflectedArgs[0] = reflect.ValueOf(b)
223	for i := range args {
224		reflectedArgs[i+1] = reflect.ValueOf(args[i])
225	}
226
227	fn.Call(reflectedArgs)
228}
229
230// Release decrements the Block's reference count, and if it is the last reference, frees it.
231func (b Block) Release() {
232	_Block_release(b)
233}
234
235// NewBlock takes a Go function that takes a Block as its first argument.
236// It returns an Block that can be called by Objective-C code.
237// The function panics if an error occurs.
238// Use [Block.Release] to free this block when it is no longer in use.
239func NewBlock(fn any) Block {
240	// get or create a block layout for the callback.
241	layout := theBlocksCache.getLayout(reflect.TypeOf(fn))
242	// we created the layout in Go memory, so we'll copy it to a newly-created Objective-C object.
243	block := Block(unsafe.Pointer(&layout)).Copy()
244	// associate the fn with the block we created before returning it.
245	return theBlocksCache.Functions.Store(block, reflect.ValueOf(fn))
246}
247
248// InvokeBlock is a convenience method for calling the implementation of a block.
249// The block implementation must return 1 value.
250func InvokeBlock[T any](block Block, args ...any) (result T, err error) {
251	block = block.Copy()
252	defer block.Release()
253
254	fn := theBlocksCache.Functions.Load(block)
255	if fn.Type().NumIn() != len(args)+1 {
256		return result, fmt.Errorf("objc: block callback expects %d arguments, got %d", fn.Type().NumIn()-1, len(args))
257	}
258
259	reflectedArgs := make([]reflect.Value, len(args)+1)
260	reflectedArgs[0] = reflect.ValueOf(block)
261	for i := range args {
262		reflectedArgs[i+1] = reflect.ValueOf(args[i])
263	}
264
265	callResult := fn.Call(reflectedArgs)
266
267	var ok bool
268	result, ok = reflect.TypeAssert[T](callResult[0])
269	if !ok {
270		return result, fmt.Errorf("objc: the returned value type %s was not %T", callResult[0].Type().String(), result)
271	}
272
273	return result, nil
274}

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