diff --git a/objc/objc_block_darwin.go b/objc/objc_block_darwin.go new file mode 100644 index 00000000..3bde9ff7 --- /dev/null +++ b/objc/objc_block_darwin.go @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors + +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] +} + +// 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() + 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 and 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 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) { + // 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..d9278627 --- /dev/null +++ b/objc/objc_block_darwin_test.go @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors + +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 + block := objc.NewBlock( + func(objc.Block) { + refCount++ + }, + ) + defer block.Release() + 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) + } +} diff --git a/objc/objc_runtime_darwin.go b/objc/objc_runtime_darwin.go index 73e1e881..aa85cc24 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -45,6 +45,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 +92,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 +389,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