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