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