Skip to content

Objc block invoke foreign - #537

Open
TotallyGamerJet wants to merge 8 commits into
ebitengine:mainfrom
TotallyGamerJet:objc-block-invoke-foreign
Open

TotallyGamerJet wants to merge 8 commits into
ebitengine:mainfrom
TotallyGamerJet:objc-block-invoke-foreign

Conversation

@TotallyGamerJet

Copy link
Copy Markdown
Collaborator

What issue is this addressing?

Closes #536

What type of issue is this addressing?

bug

What this PR does | solves

This PR allows calling objc.Block with blocks that were created from other sources

TotallyGamerJet and others added 5 commits September 30, 2026 13:47
On a cache miss, call the block's invoke pointer through the Blocks ABI,
marshalling via RegisterFunc with a signature derived from the arguments
(and T for InvokeBlock). InvokeBlock no longer copies before the lookup,
which missed for stack blocks.

Fixes ebitengine#536

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@hajimehoshi

Copy link
Copy Markdown
Member

Invoke crashes on a foreign block that returns a struct in memory.

The foreign path in Invoke calls callForeign(nil, args), which declares no result:

if !fn.IsValid() {
b.callForeign(nil, args)
return
}

When the block's C return type is a struct returned in memory (larger than 16 bytes), the caller has to pass a hidden pointer to the result buffer. No such pointer is passed here. On darwin/arm64 the block writes its result through whatever happens to be in x8. By the amd64 ABI (not tested) the block would take rdi, which holds the block pointer, as the result buffer. It would overwrite the block's isa/flags/invoke and read every argument shifted by one register.

Repro on darwin/arm64, added to testdata/block.m:

typedef struct { int64_t a, b, c, d; } Big;
void *purego_big_block(void) {
    Big (^b)(int64_t) = ^Big(int64_t x) { Big r = {x, x, x, x}; return r; };
    return Block_copy(b);
}
lib := loadBlockFixture(t)
var bigBlock func() objc.Block
purego.RegisterLibFunc(&bigBlock, lib, "purego_big_block")
block := bigBlock()
block.Invoke(int64(1)) // SIGSEGV

BLOCK_USE_STRET (1 << 29) can't guard this: it is not set on this block (flags = 0x50000000). BLOCK_HAS_SIGNATURE is set, though. The descriptor's type encoding gives the real return type, so Invoke could allocate the result or refuse the call. Note that the signature's offset in the descriptor depends on BLOCK_HAS_COPY_DISPOSE.


Filed by Claude (Claude Code), on behalf of @hajimehoshi.

@hajimehoshi

Copy link
Copy Markdown
Member

Did we support structs for blocks btw?

Comment thread objc/objc_block_darwin.go Outdated
Comment thread objc/objc_block_darwin.go
TotallyGamerJet and others added 2 commits September 30, 2026 14:40
Use the block's type encoding to validate the argument count and the kinds
of the arguments and result before calling, refuse Invoke on blocks that
return a struct (a hidden result pointer is needed), and reject func
arguments, which would consume a callback on every call.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Parse block signatures with extended block encodings (@?<...>) and quoted
names, and compare arguments and results by size and field layout instead
of only by kind, so a struct of the wrong size or an integer of the wrong
width is refused rather than called. Move the signature parsing into its
own file with unit tests, and reuse internal/strings.GoString.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +212 to +215
if f.Type.Size() == 0 {
// structs.HostLayout and other zero-sized fields have no counterpart in C.
continue
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A blank padding field such as _ [3]byte has a non-zero size, so it ends up in the layout string. The C encoding has no entry for padding, so the two sides never match. RegisterFunc's docs tell callers to add struct padding themselves, and purego's own struct tests do exactly that:

purego/func.go

Lines 104 to 106 in 87dc024

// Purego can handle the most common structs that have fields of builtin types like int8, uint16, float32, etc. However,
// it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
// that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.

purego/struct_test.go

Lines 613 to 618 in 87dc024

type BoolFloat struct {
_ structs.HostLayout
b bool
_ [3]byte // purego won't do padding for you so make sure it aligns properly with C struct
f float32
}

A Go mirror written that way is refused even though its layout matches C:

type BoolFloat struct {
	_ structs.HostLayout
	b bool
	_ [3]byte
	f float32
}

goABI returns {1111f} for this type, but encodingABI("{BoolFloat=Bf}") returns {1f}. So InvokeBlock[BoolFloat] gives a mismatch error for a block that returns struct { bool b; float f; }.

Skipping blank fields isn't enough on its own, because a _ int32 can also stand in for a real C field the caller doesn't need. Treating blank fields as opaque bytes would handle both cases: require the same total size, and require every C field to either line up with a named Go field of the same kind at the same offset, or fall inside a blank Go field. The C offsets come from the encoding with natural alignment.

Filed by Claude (Claude Code), on behalf of @hajimehoshi.

…ures

A Go struct with its padding written out as blank fields, as RegisterFunc
asks for, was refused because a type encoding has no entry for padding.
Compare the two sides by offset instead: the sizes must be equal and every
named member must be at the same offset with the same kind, while a blank
field may cover any integer members or none. Floating point members decide
which registers a struct uses, so they must match even when blank.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

objc: Block.Invoke cannot call a block Objective-C created, only one from NewBlock

2 participants