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