From 25c5827c27845f06b2da9f8e6c57236b92e4bffd Mon Sep 17 00:00:00 2001 From: James Welch Date: Tue, 16 Jul 2024 17:17:47 -0500 Subject: [PATCH 1/4] purego issue 129: Add ObjC Block Support 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. Directly addresses (closed) purego issue: https://github.com/ebitengine/purego/issues/129 --- objc/objc_block_darwin.go | 257 +++++++++++++++++++++++++++++++++ objc/objc_block_darwin_test.go | 197 +++++++++++++++++++++++++ objc/objc_runtime_darwin.go | 9 +- 3 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 objc/objc_block_darwin.go create mode 100644 objc/objc_block_darwin_test.go diff --git a/objc/objc_block_darwin.go b/objc/objc_block_darwin.go new file mode 100644 index 00000000..f666d939 --- /dev/null +++ b/objc/objc_block_darwin.go @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors +// SPDX-FileContributor: K1000000 + +package objc + +import ( + "fmt" + "reflect" + "sync" + "unsafe" + + "github.com/ebitengine/purego" +) + +const ( + // end-goal of these defaults is to get an Objectve-C memory-managed block object, + // that won't try to free() a Go pointer, but will call our custom blockFunctionCache.Delete() + // when the reference count drops to zero, so the associated function is also unreferenced. + + // blockBaseClass is the name of the class that block objects will be initialized with. + blockBaseClass = "__NSMallocBlock__" + // blockFlags is the set of flags that block objects will be initialized with. + blockFlags = blockHasCopyDispose | blockHasSignature + + // blockHasCopyDispose is a flag that tells the Objective-C runtime the block exports Copy and/or Dispose helpers. + blockHasCopyDispose = 1 << 25 + // blockHasSignature is a flag that tells the Objective-C runtime the block exports a function signature. + blockHasSignature = 1 << 30 +) + +// blockDescriptor is the Go representation of an Objective-C block descriptor. +// It is a component to be referenced by blockDescriptor. +type blockDescriptor struct { + _ uintptr + Size uintptr + _ uintptr + Dispose uintptr + Signature *uint8 +} + +// blockLayout is the Go representation of the structure abstracted by a block pointer. +// From the Objective-C point of view, a pointer to this struct is equivalent to an ID that +// references a block. +type blockLayout struct { + Isa Class + Flags uint32 + _ uint32 + Invoke uintptr + Descriptor *blockDescriptor +} + +/* +blockCache is a thread safe cache of block layouts. + +The function closures themselves are kept alive by caching them internally until the Objective-C runtime indicates that +they can be released (presumably when the reference count reaches zero.) This approach is used instead of appending the function +object to the block allocation, where it is out of the visible domain of Go's GC. +*/ +type blockFunctionCache struct { + mutex sync.RWMutex + functions map[Block]reflect.Value +} + +// Load retrieves a function (in the form of a reflect.Value, so Call can be invoked) associated with the key Block. +func (b *blockFunctionCache) Load(key Block) reflect.Value { + b.mutex.RLock() + defer b.mutex.RUnlock() + return b.functions[key] +} + +// Store associates a function (in the form of a reflect.Value) with the key Block. +func (b *blockFunctionCache) Store(key Block, value reflect.Value) Block { + b.mutex.Lock() + defer b.mutex.Unlock() + b.functions[key] = value + return key +} + +// Delete removed the function associated with the key Block. +func (b *blockFunctionCache) Delete(key Block) { + b.mutex.Lock() + defer b.mutex.Unlock() + delete(b.functions, key) +} + +// newBlockFunctionCache initilizes a new blockFunctionCache +func newBlockFunctionCache() *blockFunctionCache { + return &blockFunctionCache{functions: map[Block]reflect.Value{}} +} + +/* +blockCache is a thread safe cache of block layouts. + +It takes advantage of the block being the first argument of a block call being the block closure, +only invoking purego.NewCallback() when it encounters a new function type (rather than on for every block creation.) +This should mitigate block creations putting pressure on the callback limit. +*/ +type blockCache struct { + sync.Mutex + descriptorTemplate blockDescriptor + layoutTemplate blockLayout + layouts map[reflect.Type]blockLayout + Functions *blockFunctionCache +} + +// encode returns a blocks type as if it was given to @encode(typ) +func (*blockCache) encode(typ reflect.Type) *uint8 { + // this algorithm was copied from encodeFunc, + // but altered to panic on error, and to only accep a block-type signature. + if (typ == nil) || (typ.Kind() != reflect.Func) { + panic("objc: not a function") + } + + var encoding string + switch typ.NumOut() { + case 0: + encoding = encVoid + default: + returnType, err := encodeType(typ.Out(0), false) + if err != nil { + panic(fmt.Sprintf("objc: %v", err)) + } + encoding = returnType + } + + if (typ.NumIn() == 0) || (typ.In(0) != reflect.TypeOf(Block(0))) { + panic(fmt.Sprintf("objc: A Block implementation must take a Block as its first argument; got %v", typ.String())) + } + + encoding += encId + for i := 1; i < typ.NumIn(); i++ { + argType, err := encodeType(typ.In(i), false) + if err != nil { + panic(fmt.Sprintf("objc: %v", err)) + } + encoding = fmt.Sprint(encoding, argType) + } + + // return the encoding as a C-style string. + return &append([]uint8(encoding), 0)[0] +} + +// get layout retrieves a blockLayout VALUE constructed with the supplied function type +// It will panic if the type is not a valid block function. +func (b *blockCache) GetLayout(typ reflect.Type) blockLayout { + b.Lock() + defer b.Unlock() + + // return the cached layout, if it exists. + if layout, ok := b.layouts[typ]; ok { + return layout + } + + // otherwise: create a layout, and populate it with the default templates + layout := b.layoutTemplate + layout.Descriptor = &blockDescriptor{} + (*layout.Descriptor) = b.descriptorTemplate + + // getting the signature now will panic on invalid types before we invest in creating a callback. + layout.Descriptor.Signature = b.encode(typ) + + // create a global callback. + // this single callback can dispatch to any function with the same signature, + // since the user-provided functions are associated with the actual block allocations. + layout.Invoke = purego.NewCallback( + reflect.MakeFunc( + typ, + func(args []reflect.Value) (results []reflect.Value) { + return b.Functions.Load(args[0].Interface().(Block)).Call(args) + }, + ).Interface(), + ) + + // store it ands return it + b.layouts[typ] = layout + return layout +} + +// newBlockCache initilizes a block cache. +// It should not be called until AFTER libobjc is fully initialized. +func newBlockCache() *blockCache { + cache := &blockCache{ + descriptorTemplate: blockDescriptor{ + Size: unsafe.Sizeof(blockLayout{}), + }, + layoutTemplate: blockLayout{ + Isa: GetClass(blockBaseClass), + Flags: blockFlags, + }, + layouts: map[reflect.Type]blockLayout{}, + Functions: newBlockFunctionCache(), + } + cache.descriptorTemplate.Dispose = purego.NewCallback(cache.Functions.Delete) + return cache +} + +// blocks is the global block cache +var blocks *blockCache + +// Block is an opaque pointer to an Objective-C object containing a function with its associated closure. +type Block ID + +// Copy creates a copy of a block on the Objective-C heap (or increments the reference count if already on the heap.) +// Use Block.Release() to free the copy when it is no longer in use. +func (b Block) Copy() Block { + return _Block_copy(b) +} + +// GetImplementation pouplates a function pointer with the implementation of a Block. +// Function will panic if the Block is not kept alive while it is in use +// (possibly by using Block.Copy()). +func (b Block) GetImplementation(fptr any) { + // there is a runtime function imp_implementationWithBlock that could have been used instead, + // but experimentation has shown the returned implementation doesn't actually work as expected. + // also, it creates a new copy of the block which must be freed independently, + // which would have made this implementation more complicated than necessary. + // we know a block ID is actually a pointer to a blockLayout struct, so we'll take advantage of that. + if b != 0 { + if cfn := (*(**blockLayout)(unsafe.Pointer(&b))).Invoke; cfn != 0 { + purego.RegisterFunc(fptr, cfn) + } + } +} + +// Invoke is calls the implementation of a block. +func (b Block) Invoke(args ...any) { + InvokeBlock[struct{}](b, args...) +} + +// Release decrements the Block's reference count, and if it is the last reference, frees it. +func (b Block) Release() { + _Block_release(b) +} + +// NewBlock takes a Go function that takes a Block as its first argument. +// It returns an Block that can be called by Objective-C code. +// The function panics if an error occurs. +// Use Block.Release() to free this block when it is no longer in use. +func NewBlock(fn interface{}) Block { + // get or create a block layout for the callback. + layout := blocks.GetLayout(reflect.TypeOf(fn)) + // we created the layout in Go memory, so we'll copy it to a newly-created Objectve-C object. + block := Block(unsafe.Pointer(&layout)).Copy() + // associate the fn with the block we created before returning it. + return blocks.Functions.Store(block, reflect.ValueOf(fn)) +} + +// InvokeBlock is a convenience method for calling the implementation of a block. +func InvokeBlock[T any](block Block, args ...any) T { + block = block.Copy() + defer block.Release() + + var invoke func(Block, ...any) T + block.GetImplementation(&invoke) + return invoke(block, args...) +} diff --git a/objc/objc_block_darwin_test.go b/objc/objc_block_darwin_test.go new file mode 100644 index 00000000..edf04d0d --- /dev/null +++ b/objc/objc_block_darwin_test.go @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors +// SPDX-FileContributor: K1000000 + +package objc_test + +import ( + "fmt" + "reflect" + "testing" + "unsafe" + + "github.com/ebitengine/purego" + "github.com/ebitengine/purego/objc" +) + +func ExampleNewBlock() { + _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } + + var count = 0 + block := objc.NewBlock( + func(block objc.Block, line objc.ID, stop *bool) { + count++ + fmt.Printf("LINE %d: %s\n", count, objc.Send[string](line, objc.RegisterName("UTF8String"))) + (*stop) = (count == 3) + }, + ) + defer block.Release() + + lines := objc.ID(objc.GetClass("NSString")).Send(objc.RegisterName("stringWithUTF8String:"), "Alpha\nBeta\nGamma\nDelta\nEpsilon") + defer lines.Send(objc.RegisterName("release")) + + lines.Send(objc.RegisterName("enumerateLinesUsingBlock:"), block) + // Output: + // LINE 1: Alpha + // LINE 2: Beta + // LINE 3: Gamma +} + +func ExampleInvokeBlock() { + type vector struct { + X, Y, Z float64 + } + + block := objc.NewBlock( + func(block objc.Block, v1, v2 *vector) *vector { + return &vector{ + X: v1.Y*v2.Z - v1.Z*v2.Y, + Y: v1.Z*v2.X - v1.X*v2.Z, + Z: v1.X*v2.Y - v1.Y*v2.X, + } + }, + ) + defer block.Release() + + fmt.Println(*objc.InvokeBlock[*vector]( + block, + &vector{X: 0.1, Y: 2.3, Z: 4.5}, + &vector{X: 6.7, Y: 8.9, Z: 0.1}, + )) + // Output: {-39.82 30.14 -14.52} +} + +func TestNewBlockAndBlockGetImplementation(t *testing.T) { + t.Parallel() + values := [14]reflect.Value{ + reflect.ValueOf(true), + reflect.ValueOf(2), + reflect.ValueOf(int8(3)), + reflect.ValueOf(int16(4)), + reflect.ValueOf(int32(5)), + reflect.ValueOf(int64(6)), + reflect.ValueOf(&[]uint8("seven\x00")[0]), + reflect.ValueOf(uint(8)), + reflect.ValueOf(uint8(9)), + reflect.ValueOf(uint16(10)), + reflect.ValueOf(uint32(11)), + reflect.ValueOf(uint64(12)), + reflect.ValueOf(objc.GetClass("NSObject")), + reflect.ValueOf(unsafe.Pointer(objc.GetProtocol("NSObject"))), + } + + var argumentRecurse func([]reflect.Value, []reflect.Type, func([]reflect.Value, []reflect.Type)) + argumentRecurse = func(argumentValues []reflect.Value, argumentTypes []reflect.Type, execute func([]reflect.Value, []reflect.Type)) { + if len(argumentValues) == cap(argumentValues) { + execute(argumentValues, argumentTypes) + return + } + + argumentValues = append(argumentValues, reflect.Value{}) + argumentTypes = append(argumentTypes, nil) + for index := 0; index < len(values); index++ { + argumentValues[len(argumentValues)-1] = values[index] + argumentTypes[len(argumentTypes)-1] = values[index].Type() + argumentRecurse(argumentValues, argumentTypes, execute) + } + } + + for out := 0; out <= len(values); out++ { + returnValues := make([]reflect.Value, 0, 1) + returnTypes := make([]reflect.Type, 0, 1) + if out < len(values) { + returnValues = append(returnValues, values[out]) + returnTypes = append(returnTypes, returnValues[0].Type()) + } + + for in := 1; in < 3; in++ { + argumentValues := make([]reflect.Value, 1, in) + argumentTypes := make([]reflect.Type, 1, len(argumentValues)) + argumentValues[0] = reflect.ValueOf(objc.Block(0)) + argumentTypes[0] = argumentValues[0].Type() + + argumentRecurse(argumentValues, argumentTypes, func(argumentValues []reflect.Value, argumentTypes []reflect.Type) { + functionType := reflect.FuncOf(argumentTypes, returnTypes, false) + block := objc.NewBlock( + reflect.MakeFunc( + functionType, + func(args []reflect.Value) (results []reflect.Value) { + for index, argumentValue := range args { + if argumentValue.Interface() != argumentValues[index].Interface() { + t.Fatalf("%v: arg[%d]: %v != %v", functionType, index, argumentValue.Interface(), argumentValues[index].Interface()) + } + } + return returnValues + }, + ).Interface(), + ) + defer block.Release() + argumentValues[0] = reflect.ValueOf(block) + + fptr := reflect.New(functionType) + block.GetImplementation(fptr.Interface()) + + for index, returnValue := range fptr.Elem().Call(argumentValues) { + if returnValue.Interface() != returnValues[index].Interface() { + t.Fatalf("%v: return: %v != %v", functionType, returnValue.Interface(), returnValues[index].Interface()) + } + } + }) + } + } +} + +func TestBlockCopyAndBlockRelease(t *testing.T) { + t.Parallel() + + refCount := 0 + defer func() { + if refCount != 0 { + t.Fatalf("refCount: %d != 0", refCount) + } + }() + + block := objc.NewBlock( + func(objc.Block) { + refCount++ + }, + ) + refCount++ + + copies := make([]objc.Block, 17) + copies[0] = block + for index := 1; index < len(copies); index++ { + if refCount != index { + t.Fatalf("refCount: %d != %d", refCount, index) + } + + copies[index] = copies[index-1].Copy() + if copies[index] != block { + t.Fatalf("Block.Copy(): %v != %v", copies[index], block) + } + copies[index].Invoke() + } + + for _, copy := range copies[1:] { + copy.Release() + refCount-- + } + refCount-- + + block.Invoke() + if refCount != 1 { + t.Fatalf("refCount: %d != 1", refCount) + } + block.Release() + refCount-- + + defer func() { + if recover() == nil { + t.Fatal("Block.Release(): function was not released on refCount == 0") + } + }() + block.Invoke() +} diff --git a/objc/objc_runtime_darwin.go b/objc/objc_runtime_darwin.go index 73e1e881..1adef7e6 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors +// SPDX-FileContributor: Modified by K1000000 // Package objc is a low-level pure Go objective-c runtime. This package is easy to use incorrectly, so it is best // to use a wrapper that provides the functionality you need in a safer way. @@ -45,6 +46,8 @@ var ( object_setIvar func(obj ID, ivar Ivar, value ID) protocol_getName func(protocol *Protocol) string protocol_isEqual func(p *Protocol, p2 *Protocol) bool + _Block_copy func(Block) Block + _Block_release func(Block) ) func init() { @@ -90,6 +93,10 @@ func init() { purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual") purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar") purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar") + + purego.RegisterLibFunc(&_Block_copy, objc, "_Block_copy") + purego.RegisterLibFunc(&_Block_release, objc, "_Block_release") + blocks = newBlockCache() } // ID is an opaque pointer to some Objective-C object @@ -383,7 +390,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) { switch typ { case reflect.TypeOf(Class(0)): return encClass, nil - case reflect.TypeOf(ID(0)): + case reflect.TypeOf(ID(0)), reflect.TypeOf(Block(0)): return encId, nil case reflect.TypeOf(SEL(0)): return encSelector, nil From e7d3e21dc55b7651dcaabf4f579c410e7287db8f Mon Sep 17 00:00:00 2001 From: James Welch Date: Wed, 17 Jul 2024 14:52:09 -0500 Subject: [PATCH 2/4] ObjC Blocks: Fixup copyrights/comments. --- objc/objc_block_darwin.go | 7 +++---- objc/objc_block_darwin_test.go | 3 +-- objc/objc_runtime_darwin.go | 1 - 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/objc/objc_block_darwin.go b/objc/objc_block_darwin.go index f666d939..40eb802a 100644 --- a/objc/objc_block_darwin.go +++ b/objc/objc_block_darwin.go @@ -1,6 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2022 The Ebitengine Authors -// SPDX-FileContributor: K1000000 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors package objc @@ -172,7 +171,7 @@ func (b *blockCache) GetLayout(typ reflect.Type) blockLayout { ).Interface(), ) - // store it ands return it + // store it and return it b.layouts[typ] = layout return layout } @@ -207,7 +206,7 @@ func (b Block) Copy() Block { return _Block_copy(b) } -// GetImplementation pouplates a function pointer with the implementation of a Block. +// GetImplementation populates a function pointer with the implementation of a Block. // Function will panic if the Block is not kept alive while it is in use // (possibly by using Block.Copy()). func (b Block) GetImplementation(fptr any) { diff --git a/objc/objc_block_darwin_test.go b/objc/objc_block_darwin_test.go index edf04d0d..f1973eb5 100644 --- a/objc/objc_block_darwin_test.go +++ b/objc/objc_block_darwin_test.go @@ -1,6 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2022 The Ebitengine Authors -// SPDX-FileContributor: K1000000 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors package objc_test diff --git a/objc/objc_runtime_darwin.go b/objc/objc_runtime_darwin.go index 1adef7e6..aa85cc24 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -1,6 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors -// SPDX-FileContributor: Modified by K1000000 // Package objc is a low-level pure Go objective-c runtime. This package is easy to use incorrectly, so it is best // to use a wrapper that provides the functionality you need in a safer way. From c4394b5bb36da92e07d4f48f975285b631f1b0c8 Mon Sep 17 00:00:00 2001 From: James Welch Date: Thu, 18 Jul 2024 09:28:54 -0500 Subject: [PATCH 3/4] ObjC Blocks: Fix SIGSEV in test where panic was expected. --- objc/objc_block_darwin_test.go | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/objc/objc_block_darwin_test.go b/objc/objc_block_darwin_test.go index f1973eb5..d9278627 100644 --- a/objc/objc_block_darwin_test.go +++ b/objc/objc_block_darwin_test.go @@ -147,17 +147,12 @@ func TestBlockCopyAndBlockRelease(t *testing.T) { t.Parallel() refCount := 0 - defer func() { - if refCount != 0 { - t.Fatalf("refCount: %d != 0", refCount) - } - }() - block := objc.NewBlock( func(objc.Block) { refCount++ }, ) + defer block.Release() refCount++ copies := make([]objc.Block, 17) @@ -184,13 +179,4 @@ func TestBlockCopyAndBlockRelease(t *testing.T) { if refCount != 1 { t.Fatalf("refCount: %d != 1", refCount) } - block.Release() - refCount-- - - defer func() { - if recover() == nil { - t.Fatal("Block.Release(): function was not released on refCount == 0") - } - }() - block.Invoke() } From 8b1d9fa292cc4ef10d86b8f345c9793ff1b86064 Mon Sep 17 00:00:00 2001 From: James Welch Date: Thu, 18 Jul 2024 09:58:21 -0500 Subject: [PATCH 4/4] ObjC Blocks: Fix consitency in doc comments --- objc/objc_block_darwin.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/objc/objc_block_darwin.go b/objc/objc_block_darwin.go index 40eb802a..3bde9ff7 100644 --- a/objc/objc_block_darwin.go +++ b/objc/objc_block_darwin.go @@ -88,13 +88,11 @@ func newBlockFunctionCache() *blockFunctionCache { return &blockFunctionCache{functions: map[Block]reflect.Value{}} } -/* -blockCache is a thread safe cache of block layouts. - -It takes advantage of the block being the first argument of a block call being the block closure, -only invoking purego.NewCallback() when it encounters a new function type (rather than on for every block creation.) -This should mitigate block creations putting pressure on the callback limit. -*/ +// blockCache is a thread safe cache of block layouts. +// +// It takes advantage of the block being the first argument of a block call being the block closure, +// only invoking purego.NewCallback() when it encounters a new function type (rather than on for every block creation.) +// This should mitigate block creations putting pressure on the callback limit. type blockCache struct { sync.Mutex descriptorTemplate blockDescriptor @@ -140,7 +138,7 @@ func (*blockCache) encode(typ reflect.Type) *uint8 { return &append([]uint8(encoding), 0)[0] } -// get layout retrieves a blockLayout VALUE constructed with the supplied function type +// GetLayout retrieves a blockLayout VALUE constructed with the supplied function type // It will panic if the type is not a valid block function. func (b *blockCache) GetLayout(typ reflect.Type) blockLayout { b.Lock()