Skip to content

purego: return 64-bit callback results on 386 - #519

Open
kumagi wants to merge 3 commits into
ebitengine:mainfrom
kumagi:fix/i386-callback-i64
Open

purego: return 64-bit callback results on 386#519
kumagi wants to merge 3 commits into
ebitengine:mainfrom
kumagi:fix/i386-callback-i64

Conversation

@kumagi

@kumagi kumagi commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What issue is this addressing?

Closes #524

What type of issue is this addressing?

bug

What this PR does | solves

The 386 trampoline allocated only one result word and returned EAX alone, so int64/uint64 callbacks lost their upper half (cdecl wants EDX:EAX) and the args copy overlapped result[1..]. Widen the callbackArgs area to the full Go struct, shift the args copy by 12 bytes, and return both halves.

The 386 trampoline allocated only one result word and returned EAX
alone, so int64/uint64 callbacks lost their upper half (cdecl wants
EDX:EAX) and the args copy overlapped result[1..]. Widen the
callbackArgs area to the full Go struct, shift the args copy by 12
bytes, and return both halves.
@hajimehoshi

Copy link
Copy Markdown
Member

Can we have tests?

Add callback tests that return int64 and uint64 values whose upper word
is significant, which covers the EDX:EAX half of the 386 trampoline, and
one with stack arguments so the copied arguments are checked against the
result words.

While at it, write the last copied argument to 300(SP) instead of
clobbering 288(SP); the shift of the argument area left that word
behind.
@kumagi

kumagi commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Tests added in callback_test.go:

  • TestNewCallbackInt64Result and TestNewCallbackUint64Result return values whose upper word is significant, so the EDX half of the result is checked.
  • TestNewCallbackInt64ResultWithStackArgs passes 20 stack arguments together with a 64-bit result, which covers the shifted argument copy in the trampoline.

I verified them with GOARCH=386 CGO_ENABLED=0 go test: all three fail against the previous trampoline (the upper word came back as garbage) and pass with this change, and they also pass on amd64.

While doing that I noticed that the last copied argument was still written to 288(SP) instead of 300(SP) after the argument area was shifted, so argument slot 63 was never written and slot 60 was overwritten. That is fixed as well, along with the frame layout comments.

@hajimehoshi

Copy link
Copy Markdown
Member

Fix the test failure

RegisterFunc rejects a function with more stack arguments than
maxArgs - numOfIntegerRegisters() allows, and on ppc64le that leaves
only 7 stack slots out of maxArgs = 15. The 20 int arguments of
TestNewCallbackInt64ResultWithStackArgs overflowed this and panicked
with "purego: too many stack arguments", aborting the whole test
binary on Linux ppc64le.

Passing 12 arguments still puts every argument on the stack on 386 and
still spills onto the stack on arm and on the 64-bit architectures, so
the shifted argument copy stays covered on all platforms.
@kumagi

kumagi commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

The failure was on Linux ppc64le: TestNewCallbackInt64ResultWithStackArgs passed 20 int arguments, but ppc64le only has maxArgs (15) slots of which 8 are integer registers, so RegisterFunc panicked with purego: too many stack arguments and the panic took down the whole test binary.

The test now passes 12 arguments. That is within every supported architecture's limit and still puts all the arguments on the stack on 386, and still spills onto the stack on arm (8), s390x (7) and the other 64-bit architectures (4), so the shifted argument copy remains covered.

Verified locally:

  • GOARCH=386 CGO_ENABLED=0: all three tests fail against the previous trampoline (the upper word came back as garbage) and pass with this change.
  • GOARCH=ppc64le, GOARCH=riscv64 and GOARCH=s390x (s390x needs Go 1.27 for CGO_ENABLED=0) under qemu: the tests run and pass, and the ppc64le panic is gone.
  • GOARCH=amd64 with CGO_ENABLED=0/CGO_ENABLED=1 and with -gcflags=all=-N -l: pass, and gofmt -s -l plus go vet are clean.

@hajimehoshi hajimehoshi left a comment

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.

LGTM (but TBH I don't fully understand the code...)

@TotallyGamerJet PTAL

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.

386 callback trampoline drops the upper half of int64/uint64 returns

2 participants