detached 36d25bb

Add support for Objc blocks (#325)

Adds new type, objc.Block, which is an objc.ID referencing an
Objective-C "block" function pointer.

Adds methods to create a Block from a Go function value,
get a Go function value from a block, directly invoke
a block function, and handle Objective-C memory management
(e.g. Copy/Release).

Mitigates pressure on purego Callback limit by relying on the
fact the first argument passed to a block implementation is the
block itself. This allows for a single callback to handle every
block instance that has the same signature, by way of keeping
an association between the Go func value and the block instance
it was used to create.

Was refactored from code in a different personal project to
better fit the purego convetions and architecture.

Closes #129

---------

Co-authored-by: James Welch <jwelch@ingrooves.com>
commit
36d25bbecce0a6b485671db483afa64974c83c01
author
Luc · · 2025-07-06 13:03 UTC
committer
GitHub · · 2025-07-06 13:03 UTC
parent
469dacd
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.9.0-alpha.9
4 files · +430 -3
A objc/objc_block_darwin.go +267 -0
@@ -0,0 +1,267 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package objc
5+
6+import (
7+ "fmt"
8+ "reflect"
9+ "sync"
10+ "unsafe"
11+
12+ "github.com/ebitengine/purego"
13+)
14+
15+const (
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
36+type 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
49+type 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.
62+type 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.
68+func (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.
75+func (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.
83+func (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
90+func 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.
99+type 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)
108+func (*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.TypeOf(Block(0)) {
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.
146+func (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+ return b.Functions.Load(args[0].Interface().(Block)).Call(args)
171+ },
172+ ).Interface(),
173+ )
174+
175+ // store it and return it
176+ b.layouts[typ] = layout
177+ return layout
178+}
179+
180+// newBlockCache initializes a block cache.
181+// It should not be called until AFTER libobjc is fully initialized.
182+func newBlockCache() *blockCache {
183+ cache := &blockCache{
184+ descriptorTemplate: blockDescriptor{
185+ size: unsafe.Sizeof(blockLayout{}),
186+ },
187+ layoutTemplate: blockLayout{
188+ isa: GetClass(blockBaseClass),
189+ flags: blockFlags,
190+ },
191+ layouts: map[reflect.Type]blockLayout{},
192+ Functions: newBlockFunctionCache(),
193+ }
194+ cache.descriptorTemplate.dispose = purego.NewCallback(cache.Functions.Delete)
195+ return cache
196+}
197+
198+// theBlocksCache is the global block cache
199+var theBlocksCache *blockCache
200+
201+// Block is an opaque pointer to an Objective-C object containing a function with its associated closure.
202+type Block ID
203+
204+// Copy creates a copy of a block on the Objective-C heap (or increments the reference count if already on the heap).
205+// Use [Block.Release] to free the copy when it is no longer in use.
206+func (b Block) Copy() Block {
207+ return _Block_copy(b)
208+}
209+
210+// Invoke calls the implementation of a block.
211+func (b Block) Invoke(args ...any) {
212+ fn := theBlocksCache.Functions.Load(b)
213+
214+ reflectedArgs := make([]reflect.Value, len(args)+1)
215+ reflectedArgs[0] = reflect.ValueOf(b)
216+ for i := range args {
217+ reflectedArgs[i+1] = reflect.ValueOf(args[i])
218+ }
219+
220+ fn.Call(reflectedArgs)
221+}
222+
223+// Release decrements the Block's reference count, and if it is the last reference, frees it.
224+func (b Block) Release() {
225+ _Block_release(b)
226+}
227+
228+// NewBlock takes a Go function that takes a Block as its first argument.
229+// It returns an Block that can be called by Objective-C code.
230+// The function panics if an error occurs.
231+// Use [Block.Release] to free this block when it is no longer in use.
232+func NewBlock(fn any) Block {
233+ // get or create a block layout for the callback.
234+ layout := theBlocksCache.getLayout(reflect.TypeOf(fn))
235+ // we created the layout in Go memory, so we'll copy it to a newly-created Objective-C object.
236+ block := Block(unsafe.Pointer(&layout)).Copy()
237+ // associate the fn with the block we created before returning it.
238+ return theBlocksCache.Functions.Store(block, reflect.ValueOf(fn))
239+}
240+
241+// InvokeBlock is a convenience method for calling the implementation of a block.
242+// The block implementation must return 1 value.
243+func InvokeBlock[T any](block Block, args ...any) (result T, err error) {
244+ block = block.Copy()
245+ defer block.Release()
246+
247+ fn := theBlocksCache.Functions.Load(block)
248+ if fn.Type().NumIn() != len(args)+1 {
249+ return result, fmt.Errorf("objc: block callback expects %d arguments, got %d", fn.Type().NumIn()-1, len(args))
250+ }
251+
252+ reflectedArgs := make([]reflect.Value, len(args)+1)
253+ reflectedArgs[0] = reflect.ValueOf(block)
254+ for i := range args {
255+ reflectedArgs[i+1] = reflect.ValueOf(args[i])
256+ }
257+
258+ callResult := fn.Call(reflectedArgs)
259+
260+ var ok bool
261+ result, ok = callResult[0].Interface().(T)
262+ if !ok {
263+ return result, fmt.Errorf("objc: the returned value type %s was not %T", callResult[0].Type().String(), result)
264+ }
265+
266+ return result, nil
267+}
@@ -0,0 +1,267 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package objc
5+
6+import (
7+ "fmt"
8+ "reflect"
9+ "sync"
10+ "unsafe"
11+
12+ "github.com/ebitengine/purego"
13+)
14+
15+const (
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
36+type 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
49+type 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.
62+type 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.
68+func (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.
75+func (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.
83+func (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
90+func 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.
99+type 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)
108+func (*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.TypeOf(Block(0)) {
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.
146+func (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+ return b.Functions.Load(args[0].Interface().(Block)).Call(args)
171+ },
172+ ).Interface(),
173+ )
174+
175+ // store it and return it
176+ b.layouts[typ] = layout
177+ return layout
178+}
179+
180+// newBlockCache initializes a block cache.
181+// It should not be called until AFTER libobjc is fully initialized.
182+func newBlockCache() *blockCache {
183+ cache := &blockCache{
184+ descriptorTemplate: blockDescriptor{
185+ size: unsafe.Sizeof(blockLayout{}),
186+ },
187+ layoutTemplate: blockLayout{
188+ isa: GetClass(blockBaseClass),
189+ flags: blockFlags,
190+ },
191+ layouts: map[reflect.Type]blockLayout{},
192+ Functions: newBlockFunctionCache(),
193+ }
194+ cache.descriptorTemplate.dispose = purego.NewCallback(cache.Functions.Delete)
195+ return cache
196+}
197+
198+// theBlocksCache is the global block cache
199+var theBlocksCache *blockCache
200+
201+// Block is an opaque pointer to an Objective-C object containing a function with its associated closure.
202+type Block ID
203+
204+// Copy creates a copy of a block on the Objective-C heap (or increments the reference count if already on the heap).
205+// Use [Block.Release] to free the copy when it is no longer in use.
206+func (b Block) Copy() Block {
207+ return _Block_copy(b)
208+}
209+
210+// Invoke calls the implementation of a block.
211+func (b Block) Invoke(args ...any) {
212+ fn := theBlocksCache.Functions.Load(b)
213+
214+ reflectedArgs := make([]reflect.Value, len(args)+1)
215+ reflectedArgs[0] = reflect.ValueOf(b)
216+ for i := range args {
217+ reflectedArgs[i+1] = reflect.ValueOf(args[i])
218+ }
219+
220+ fn.Call(reflectedArgs)
221+}
222+
223+// Release decrements the Block's reference count, and if it is the last reference, frees it.
224+func (b Block) Release() {
225+ _Block_release(b)
226+}
227+
228+// NewBlock takes a Go function that takes a Block as its first argument.
229+// It returns an Block that can be called by Objective-C code.
230+// The function panics if an error occurs.
231+// Use [Block.Release] to free this block when it is no longer in use.
232+func NewBlock(fn any) Block {
233+ // get or create a block layout for the callback.
234+ layout := theBlocksCache.getLayout(reflect.TypeOf(fn))
235+ // we created the layout in Go memory, so we'll copy it to a newly-created Objective-C object.
236+ block := Block(unsafe.Pointer(&layout)).Copy()
237+ // associate the fn with the block we created before returning it.
238+ return theBlocksCache.Functions.Store(block, reflect.ValueOf(fn))
239+}
240+
241+// InvokeBlock is a convenience method for calling the implementation of a block.
242+// The block implementation must return 1 value.
243+func InvokeBlock[T any](block Block, args ...any) (result T, err error) {
244+ block = block.Copy()
245+ defer block.Release()
246+
247+ fn := theBlocksCache.Functions.Load(block)
248+ if fn.Type().NumIn() != len(args)+1 {
249+ return result, fmt.Errorf("objc: block callback expects %d arguments, got %d", fn.Type().NumIn()-1, len(args))
250+ }
251+
252+ reflectedArgs := make([]reflect.Value, len(args)+1)
253+ reflectedArgs[0] = reflect.ValueOf(block)
254+ for i := range args {
255+ reflectedArgs[i+1] = reflect.ValueOf(args[i])
256+ }
257+
258+ callResult := fn.Call(reflectedArgs)
259+
260+ var ok bool
261+ result, ok = callResult[0].Interface().(T)
262+ if !ok {
263+ return result, fmt.Errorf("objc: the returned value type %s was not %T", callResult[0].Type().String(), result)
264+ }
265+
266+ return result, nil
267+}
A objc/objc_block_darwin_test.go +154 -0
@@ -0,0 +1,154 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package objc_test
5+
6+import (
7+ "fmt"
8+ "testing"
9+
10+ "github.com/ebitengine/purego"
11+ "github.com/ebitengine/purego/objc"
12+)
13+
14+func ExampleNewBlock() {
15+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
16+ if err != nil {
17+ panic(err)
18+ }
19+
20+ var count = 0
21+ block := objc.NewBlock(
22+ func(block objc.Block, line objc.ID, stop *bool) {
23+ count++
24+ fmt.Printf("LINE %d: %s\n", count, objc.Send[string](line, objc.RegisterName("UTF8String")))
25+ *stop = count == 3
26+ },
27+ )
28+ defer block.Release()
29+
30+ lines := objc.ID(objc.GetClass("NSString")).Send(objc.RegisterName("stringWithUTF8String:"), "Alpha\nBeta\nGamma\nDelta\nEpsilon")
31+ defer lines.Send(objc.RegisterName("release"))
32+
33+ lines.Send(objc.RegisterName("enumerateLinesUsingBlock:"), block)
34+ // Output:
35+ // LINE 1: Alpha
36+ // LINE 2: Beta
37+ // LINE 3: Gamma
38+}
39+
40+func ExampleInvokeBlock() {
41+ type vector struct {
42+ X, Y, Z float64
43+ }
44+
45+ block := objc.NewBlock(
46+ func(block objc.Block, v1, v2 *vector) *vector {
47+ return &vector{
48+ X: v1.Y*v2.Z - v1.Z*v2.Y,
49+ Y: v1.Z*v2.X - v1.X*v2.Z,
50+ Z: v1.X*v2.Y - v1.Y*v2.X,
51+ }
52+ },
53+ )
54+ defer block.Release()
55+
56+ result, err := objc.InvokeBlock[*vector](
57+ block,
58+ &vector{X: 0.1, Y: 2.3, Z: 4.5},
59+ &vector{X: 6.7, Y: 8.9, Z: 0.1},
60+ )
61+
62+ fmt.Println(*result, err)
63+ // Output: {-39.82 30.14 -14.52} <nil>
64+}
65+
66+func TestInvoke(t *testing.T) {
67+ t.Run("return an error when passing an invalid number of arguments", func(t *testing.T) {
68+ block := objc.NewBlock(func(_ objc.Block, a int32, b int32) int32 {
69+ return a + b
70+ })
71+ defer block.Release()
72+
73+ if _, err := objc.InvokeBlock[int32](block, int32(8)); err == nil {
74+ t.Fatal(err)
75+ }
76+ })
77+
78+ t.Run("return an error when passing an invalid return type", func(t *testing.T) {
79+ block := objc.NewBlock(func(_ objc.Block, a int32, b int32) int32 {
80+ return a + b
81+ })
82+ defer block.Release()
83+
84+ if _, err := objc.InvokeBlock[string](block, int32(8), int32(2)); err == nil {
85+ t.Fatal(err)
86+ }
87+ })
88+
89+ t.Run("add two int32's and returns the result", func(t *testing.T) {
90+ block := objc.NewBlock(func(_ objc.Block, a int32, b int32) int32 {
91+ return a + b
92+ })
93+ defer block.Release()
94+
95+ result, err := objc.InvokeBlock[int32](block, int32(8), int32(2))
96+ if err != nil {
97+ t.Fatal(err)
98+ }
99+ if result != 10 {
100+ t.Fatalf("expected 10, got %d", result)
101+ }
102+ })
103+
104+ t.Run("add two int32's and store the result in a variable", func(t *testing.T) {
105+ var result int32
106+ block := objc.NewBlock(func(_ objc.Block, a int32, b int32) {
107+ result = a + b
108+ })
109+ defer block.Release()
110+
111+ block.Invoke(int32(8), int32(2))
112+ if result != 10 {
113+ t.Fatalf("expected 10, got %d", result)
114+ }
115+ })
116+}
117+
118+func TestBlockCopyAndBlockRelease(t *testing.T) {
119+ t.Parallel()
120+
121+ var refCount int
122+ block := objc.NewBlock(
123+ func(objc.Block) {
124+ refCount++
125+ },
126+ )
127+ defer block.Release()
128+ refCount++
129+
130+ copies := make([]objc.Block, 17)
131+ copies[0] = block
132+ for index := 1; index < len(copies); index++ {
133+ if refCount != index {
134+ t.Fatalf("refCount: %d != %d", refCount, index)
135+ }
136+
137+ copies[index] = copies[index-1].Copy()
138+ if copies[index] != block {
139+ t.Fatalf("Block.Copy(): %v != %v", copies[index], block)
140+ }
141+ copies[index].Invoke()
142+ }
143+
144+ for _, copy := range copies[1:] {
145+ copy.Release()
146+ refCount--
147+ }
148+ refCount--
149+
150+ block.Invoke()
151+ if refCount != 1 {
152+ t.Fatalf("refCount: %d != 1", refCount)
153+ }
154+}
@@ -0,0 +1,154 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package objc_test
5+
6+import (
7+ "fmt"
8+ "testing"
9+
10+ "github.com/ebitengine/purego"
11+ "github.com/ebitengine/purego/objc"
12+)
13+
14+func ExampleNewBlock() {
15+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
16+ if err != nil {
17+ panic(err)
18+ }
19+
20+ var count = 0
21+ block := objc.NewBlock(
22+ func(block objc.Block, line objc.ID, stop *bool) {
23+ count++
24+ fmt.Printf("LINE %d: %s\n", count, objc.Send[string](line, objc.RegisterName("UTF8String")))
25+ *stop = count == 3
26+ },
27+ )
28+ defer block.Release()
29+
30+ lines := objc.ID(objc.GetClass("NSString")).Send(objc.RegisterName("stringWithUTF8String:"), "Alpha\nBeta\nGamma\nDelta\nEpsilon")
31+ defer lines.Send(objc.RegisterName("release"))
32+
33+ lines.Send(objc.RegisterName("enumerateLinesUsingBlock:"), block)
34+ // Output:
35+ // LINE 1: Alpha
36+ // LINE 2: Beta
37+ // LINE 3: Gamma
38+}
39+
40+func ExampleInvokeBlock() {
41+ type vector struct {
42+ X, Y, Z float64
43+ }
44+
45+ block := objc.NewBlock(
46+ func(block objc.Block, v1, v2 *vector) *vector {
47+ return &vector{
48+ X: v1.Y*v2.Z - v1.Z*v2.Y,
49+ Y: v1.Z*v2.X - v1.X*v2.Z,
50+ Z: v1.X*v2.Y - v1.Y*v2.X,
51+ }
52+ },
53+ )
54+ defer block.Release()
55+
56+ result, err := objc.InvokeBlock[*vector](
57+ block,
58+ &vector{X: 0.1, Y: 2.3, Z: 4.5},
59+ &vector{X: 6.7, Y: 8.9, Z: 0.1},
60+ )
61+
62+ fmt.Println(*result, err)
63+ // Output: {-39.82 30.14 -14.52} <nil>
64+}
65+
66+func TestInvoke(t *testing.T) {
67+ t.Run("return an error when passing an invalid number of arguments", func(t *testing.T) {
68+ block := objc.NewBlock(func(_ objc.Block, a int32, b int32) int32 {
69+ return a + b
70+ })
71+ defer block.Release()
72+
73+ if _, err := objc.InvokeBlock[int32](block, int32(8)); err == nil {
74+ t.Fatal(err)
75+ }
76+ })
77+
78+ t.Run("return an error when passing an invalid return type", func(t *testing.T) {
79+ block := objc.NewBlock(func(_ objc.Block, a int32, b int32) int32 {
80+ return a + b
81+ })
82+ defer block.Release()
83+
84+ if _, err := objc.InvokeBlock[string](block, int32(8), int32(2)); err == nil {
85+ t.Fatal(err)
86+ }
87+ })
88+
89+ t.Run("add two int32's and returns the result", func(t *testing.T) {
90+ block := objc.NewBlock(func(_ objc.Block, a int32, b int32) int32 {
91+ return a + b
92+ })
93+ defer block.Release()
94+
95+ result, err := objc.InvokeBlock[int32](block, int32(8), int32(2))
96+ if err != nil {
97+ t.Fatal(err)
98+ }
99+ if result != 10 {
100+ t.Fatalf("expected 10, got %d", result)
101+ }
102+ })
103+
104+ t.Run("add two int32's and store the result in a variable", func(t *testing.T) {
105+ var result int32
106+ block := objc.NewBlock(func(_ objc.Block, a int32, b int32) {
107+ result = a + b
108+ })
109+ defer block.Release()
110+
111+ block.Invoke(int32(8), int32(2))
112+ if result != 10 {
113+ t.Fatalf("expected 10, got %d", result)
114+ }
115+ })
116+}
117+
118+func TestBlockCopyAndBlockRelease(t *testing.T) {
119+ t.Parallel()
120+
121+ var refCount int
122+ block := objc.NewBlock(
123+ func(objc.Block) {
124+ refCount++
125+ },
126+ )
127+ defer block.Release()
128+ refCount++
129+
130+ copies := make([]objc.Block, 17)
131+ copies[0] = block
132+ for index := 1; index < len(copies); index++ {
133+ if refCount != index {
134+ t.Fatalf("refCount: %d != %d", refCount, index)
135+ }
136+
137+ copies[index] = copies[index-1].Copy()
138+ if copies[index] != block {
139+ t.Fatalf("Block.Copy(): %v != %v", copies[index], block)
140+ }
141+ copies[index].Invoke()
142+ }
143+
144+ for _, copy := range copies[1:] {
145+ copy.Release()
146+ refCount--
147+ }
148+ refCount--
149+
150+ block.Invoke()
151+ if refCount != 1 {
152+ t.Fatalf("refCount: %d != 1", refCount)
153+ }
154+}
M objc/objc_runtime_darwin.go +8 -2
@@ -57,7 +57,9 @@ var (
5757 property_getName func(p Property) string
5858 property_getAttributes func(p Property) string
5959
60- free func(ptr unsafe.Pointer)
60+ free func(ptr unsafe.Pointer)
61+ _Block_copy func(Block) Block
62+ _Block_release func(Block)
6163 )
6264
6365 func init() {
@@ -114,6 +116,10 @@ func init() {
114116 purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
115117 purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
116118 purego.RegisterLibFunc(&free, purego.RTLD_DEFAULT, "free")
119+
120+ purego.RegisterLibFunc(&_Block_copy, objc, "_Block_copy")
121+ purego.RegisterLibFunc(&_Block_release, objc, "_Block_release")
122+ theBlocksCache = newBlockCache()
117123 }
118124
119125 // ID is an opaque pointer to some Objective-C object
@@ -407,7 +413,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
407413 switch typ {
408414 case reflect.TypeOf(Class(0)):
409415 return encClass, nil
410- case reflect.TypeOf(ID(0)):
416+ case reflect.TypeOf(ID(0)), reflect.TypeOf(Block(0)):
411417 return encId, nil
412418 case reflect.TypeOf(SEL(0)):
413419 return encSelector, nil
@@ -57,7 +57,9 @@ var (
57 property_getName func(p Property) string57 property_getName func(p Property) string
58 property_getAttributes func(p Property) string58 property_getAttributes func(p Property) string
59 59
60- free func(ptr unsafe.Pointer)60+ free func(ptr unsafe.Pointer)
61+ _Block_copy func(Block) Block
62+ _Block_release func(Block)
61 )63 )
62 64
63 func init() {65 func init() {
@@ -114,6 +116,10 @@ func init() {
114 purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")116 purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
115 purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")117 purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
116 purego.RegisterLibFunc(&free, purego.RTLD_DEFAULT, "free")118 purego.RegisterLibFunc(&free, purego.RTLD_DEFAULT, "free")
119+
120+ purego.RegisterLibFunc(&_Block_copy, objc, "_Block_copy")
121+ purego.RegisterLibFunc(&_Block_release, objc, "_Block_release")
122+ theBlocksCache = newBlockCache()
117 }123 }
118 124
119 // ID is an opaque pointer to some Objective-C object125 // ID is an opaque pointer to some Objective-C object
@@ -407,7 +413,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
407 switch typ {413 switch typ {
408 case reflect.TypeOf(Class(0)):414 case reflect.TypeOf(Class(0)):
409 return encClass, nil415 return encClass, nil
410- case reflect.TypeOf(ID(0)):416+ case reflect.TypeOf(ID(0)), reflect.TypeOf(Block(0)):
411 return encId, nil417 return encId, nil
412 case reflect.TypeOf(SEL(0)):418 case reflect.TypeOf(SEL(0)):
413 return encSelector, nil419 return encSelector, nil
M objc/objc_runtime_darwin_test.go +1 -1
@@ -139,7 +139,7 @@ func ExampleID_SendSuper() {
139139
140140 func TestSend(t *testing.T) {
141141 // NSNumber comes from Foundation so make sure we have linked to that framework.
142- _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL)
142+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
143143 if err != nil {
144144 t.Fatal(err)
145145 }
@@ -139,7 +139,7 @@ func ExampleID_SendSuper() {
139 139
140 func TestSend(t *testing.T) {140 func TestSend(t *testing.T) {
141 // NSNumber comes from Foundation so make sure we have linked to that framework.141 // NSNumber comes from Foundation so make sure we have linked to that framework.
142- _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL)142+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
143 if err != nil {143 if err != nil {
144 t.Fatal(err)144 t.Fatal(err)
145 }145 }

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