Skip to content
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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Conversation

K1000000
Copy link

@K1000000 K1000000 commented Jul 17, 2024

What issue is this addressing?

Updates #129

What type of issue is this addressing?

feature

What this PR does

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.

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:
ebitengine#129
objc/objc_block_darwin.go Outdated Show resolved Hide resolved
objc/objc_block_darwin.go Outdated Show resolved Hide resolved
objc/objc_block_darwin_test.go Outdated Show resolved Hide resolved
objc/objc_runtime_darwin.go Outdated Show resolved Hide resolved
@hajimehoshi
Copy link
Member

Please address on test failures

Copy link
Member

@hajimehoshi hajimehoshi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TotallyGamerJet Do you have any insights?

objc/objc_block_darwin_test.go Show resolved Hide resolved
objc/objc_block_darwin.go Outdated Show resolved Hide resolved
objc/objc_block_darwin.go Outdated Show resolved Hide resolved
// otherwise: create a layout, and populate it with the default templates
layout := b.layoutTemplate
layout.Descriptor = &blockDescriptor{}
(*layout.Descriptor) = b.descriptorTemplate
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: these parenthesis aren't necessary

// 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to write this as:

if b == 0 {
    return
}
if cfn := (*(**blockLayout)(unsafe.Pointer(&b))).Invoke; cfn == 0 {
    return
}
purego.RegisterFunc(fptr, cfn)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIll fix. Note that the cfn := clause would need to be pulled out of the if block to actually work, but simple enough.

var blocks *blockCache

// Block is an opaque pointer to an Objective-C object containing a function with its associated closure.
type Block ID
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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 uintptr, so defining a block as a pointer type would complicate down-casting. I believe the choice to define ID as uintptr instead of *struct{...} (which would better align to what it actually is in Objective-C) was to allow receiver methods to be defined (since go does not allow receiver methods on pointer type aliases), but that's just speculation/intuition.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does have methods (most importantly Copy() and Release() for lifetime management), but taking your thoughts into account, this could be refactored to behave like Protocol, which is used as a struct pointer, even though it, too, is actually a first-class object?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

this could be refactored to behave like Protocol

Yes that's what I'm thinking

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: neither of these parentheses are necessary

)

func ExampleNewBlock() {
_, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it isn't necessary it's good to add purego.RTLD_NOW| as that's the proper way to call dlopen

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, will fix. I actually copied the syntax from the existing load of libobjc.dylib.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix it there too

// 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should this be exported?

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Author

@K1000000 K1000000 Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Perhaps then, just replace this method with Implementation() (to treat it like a read-only property) that returns a uintptr, and allow the user to call RegisterFunc() at their own discretion (keeping in mind, the lifetime caveats, of course?), with the option to use just use an Invoke() or BlockInvoke[]() that have already been implemented with lifetime-safety in mind?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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


// 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

// 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

if type.Kind() == reflect.Func && ((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, err := encodeFunc(fn)
if err != nil {
   panic(fmt.Sprintf("objc: failed to encode Block %s", err))
}
``

Copy link
Author

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. Thank you.

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.
*/
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants