-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ObjC Block Support #261
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just do this check first and then call the encodeFunc instead of copying the code.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem is, encodeFunc (as it stands) has a check that enforces an Objective-C method signature, which is not the same as a block signature. Maybe worth splitting up that functon from it's check first, and having both implementations share? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you think you can find a good solution go for it otherwise it might be ok to just leave it how you have it now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do. Thank you. |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be exported as it is a method on an unexpected type |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: these parenthesis aren't necessary |
||
|
||
// 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for keeping this as an opaque pointer instead of just *blockLayout? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A "Block" in Objective-C is first-class object (e.g. can be cast to an ID and sent messages like any other object.) The problem is, ID in purego's objc implementation has a base of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are people going to actually cast Block to ID? Block doesn't have any methods does it? Of course if a class extended Block then you'd probably want ID but then are you using it as a Block still? Perhaps a Block.ID() method and BlockFromID() function that does the unsafe cast? Perhaps that can be added later as I'm not confident people need to use a Block as an ID There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does have methods (most importantly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean there are no objc methods that users are trying to call.
Yes that's what I'm thinking |
||
|
||
// 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 was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why should this be exported? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't actually happy with it either. It was meant to provide provide synergy with purego's RegisterFunc in the block case, but has potential for misuse because of blocks having managed lifetimes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If someone wants to call a Block directly can't they just call RegisterFunc? It's just a C function with a objc.Block as the first argument current? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. Perhaps then, just replace this method with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have a use case where grabbing the raw uintptr to call directly is helpful? I want to limit the exposed API to only the necessary parts |
||
// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's better to write this as:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WIll fix. Note that the |
||
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...) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
//
style comments for consistency.