Skip to content

purego: pass non-HFA arm64 structs in integer registers - #517

Open
kumagi wants to merge 4 commits into
ebitengine:mainfrom
kumagi:fix/arm64-mixed-struct
Open

purego: pass non-HFA arm64 structs in integer registers#517
kumagi wants to merge 4 commits into
ebitengine:mainfrom
kumagi:fix/arm64-mixed-struct

Conversation

@kumagi

@kumagi kumagi commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What issue is this addressing?

Closes #522

What type of issue is this addressing?

bug

What this PR does | solves

placeRegistersArm64 routed float/64-bit members straight to FP or integer registers by kind, so mixed structs such as {int64; float64} went out on x0/v0 while AAPCS64 (and our own getCallbackStruct) expect them packed into x0/x1. Copy the in-memory image eightbyte by eightbyte for non-HFA/HVA aggregates of 16 bytes or less.

@hajimehoshi

Copy link
Copy Markdown
Member

Fix the test fail

placeRegistersArm64 routed float/64-bit members straight to FP or
integer registers by kind, so mixed structs such as {int64; float64}
went out on x0/v0 while AAPCS64 (and our own getCallbackStruct)
expect them packed into x0/x1. Copy the in-memory image eightbyte by
eightbyte for non-HFA/HVA aggregates of 16 bytes or less.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new packing path can still split a single struct across registers and stack on integer-register overflow, conflicting with the file’s own all-or-nothing overflow handling in getCallbackStruct.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adjusts ARM64 (non-Darwin) struct argument register placement to match AAPCS64 for small non-HFA/HVA aggregates, fixing mixed-member structs that were previously split across GPR/FPR.

Changes:

  • Adds a non-HFA/HVA <=16-byte fast path that copies the struct’s in-memory image in 8-byte chunks into integer registers.
  • Avoids routing fields to FP vs integer registers purely by kind for these small non-HFA/HVA aggregates.
File summaries
File Description
struct_arm64.go Packs small non-HFA/HVA aggregates into 1–2 integer-register chunks based on in-memory layout (ARM64 ABI alignment).
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread struct_arm64.go
Comment thread struct_arm64.go
…stack

addStruct packed small non-HFA/HVA structs into integer registers
eightbyte by eightbyte, so when fewer registers than eightbytes
remained, the struct was split across the last register and the stack.
AAPCS64 and getCallbackStruct instead pass such a struct entirely on
the stack, so exhaust the integer registers first to force whole-struct
stack placement (Darwin keeps its splitting convention).

Also add arm64 round-trip tests for struct{int64; double}, including
the register-overflow case, to cover the ebitengine#522 regression.
@kumagi

kumagi commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both review comments:

  1. Small non-HFA/HVA structs that don't fit entirely in the remaining integer registers are now forced to the stack as a whole. addStruct exhausts the integer registers before placement in that case, mirroring the all-or-nothing rule in getCallbackStruct (AAPCS64). Darwin is unchanged since Apple's ABI does allow splitting a struct between registers and the stack.
  2. Added arm64 round-trip tests for struct{ int64; double } (issue arm64 sends mixed non-HFA structs on x0/v0 instead of x0/x1 #522's case): a plain identity call plus a variant with seven leading int64 arguments that exercises the register-overflow path. Both run as Go→C identity (RegisterLibFunc) and Go→C→Go callback (GoCallbackFunc).

Verified under qemu-aarch64 against real compiled C: the new tests fail on the pre-fix code (float dropped to v0 / struct split across x7+stack) and pass now. The full test suite passes on linux/amd64 and on linux/arm64 (CGO enabled and disabled). Tests should be green.

Comment thread struct_test.go Outdated
Apple clang makes the same choice as AAPCS64 for a small non-HFA
struct that no longer fits in the integer registers: both eightbytes
go to the stack and the remaining integer register is consumed, as
confirmed with clang -S for struct{int64_t; double} following seven
int64_t arguments. Purego's Darwin path reaches the same layout
through the stack-argument bundling before any register placement, so
drop the skip and let macOS CI cover it. Also correct the addStruct
comment to point at shouldBundleStackArgs instead of claiming that
Darwin splits such a struct across registers and stack.
@kumagi

kumagi commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Followed up on the Darwin question about IdentityInt64AndDoubleAfterRegisters.

I checked what Apple's arm64 ABI actually does for f(int64_t x7, struct { int64_t a; double b; } s) by inspecting clang --target=arm64-apple-macos -S output: the callee reads both eightbytes from the incoming stack area (ldp x0, x1, [sp]), and in a variant with a trailing int64_t after the struct that argument also lands on the stack, i.e. Darwin consumes the last integer register and passes the whole struct on the stack — the same all-or-nothing choice as AAPCS64 for this type. Purego's Darwin path already produces exactly that layout: a small non-HFA struct that no longer fits is diverted to bundleStackArgs before any register placement (the path exercised by the existing stack_8int_*struct* cases in TestABI_ArgumentPassing on macOS), and getCallbackStruct/callbackArgFromStack read it back the same way.

So the runtime.GOOS != "darwin" skip was unnecessary; I removed it, and the test now runs on Darwin too. I also corrected the addStruct comment, which wrongly suggested Darwin splits such a struct across registers and stack. Re-verified: the full suite passes on linux/amd64 and on linux/arm64 under qemu, and the new tests still fail on the pre-fix code.

The non-HFA/HVA <=16-byte packing added to placeRegistersArm64
re-implemented the eightbyte loop that copyStruct8ByteChunks already
provided for Darwin. Both conventions send a small composite as
consecutive chunks of its in-memory image, so call the helper from
both and drop its Darwin-only assertion; the callers already pin the
platform they belong to.
@kumagi

kumagi commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

All review threads are addressed and the branch applies cleanly on top of main.

  • Overflow: addStruct now exhausts the integer registers when a small non-HFA/HVA composite needs more eightbytes than remain, so the whole struct goes on the stack — the same all-or-nothing rule getCallbackStruct already implements.
  • Coverage: TestRegisterFunc_structArgs round-trips struct{ int64; double } (IdentityInt64AndDouble) both as a Go to C identity call and through a Go callback, and IdentityInt64AndDoubleAfterRegisters (seven leading int64 arguments, only x7 left) pins the whole-struct stack spill.
  • Darwin: clang --target=arm64-apple-macos -S shows Apple's ABI making the same choice for this shape — both eightbytes land in the incoming argument stack and the last integer register is consumed — so the Darwin skip is removed and macOS CI verifies it as well. Purego reaches that layout through shouldBundleStackArgs/bundleStackArgs before any register placement.

One follow-up cleanup: the eightbyte loop added to placeRegistersArm64 duplicated copyStruct8ByteChunks, so the AAPCS64 packing path and the Darwin byte-packing path now share that helper. Its Darwin-only assertion is gone because every caller already pins the platform it belongs to.

Verification: gofmt -s, go vet, the documented cross-compilations, and the full test suite on linux/amd64 (Cgo enabled and disabled, and with -gcflags=all=-N -l). On linux/arm64 the suite runs under qemu-aarch64 against a real cross-compiled C library; the new tests still fail without the packing fix (B comes back as 0) and pass with it.

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.

arm64 sends mixed non-HFA structs on x0/v0 instead of x0/x1

3 participants