Skip to content

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

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

kumagi wants to merge 5 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

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 Outdated
Comment thread struct_arm64.go Outdated
@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
@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.

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

Comment thread struct_arm64.go Outdated
Comment thread struct_test.go Outdated
Comment thread struct_arm64.go Outdated
Comment thread struct_test.go Outdated
@kumagi

kumagi commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Followed up on the latest review round:

  • Shortened the register-overflow comment in addStruct and the packing comment in placeRegistersArm64, and trimmed the test comments accordingly.
  • copyStruct8ByteChunks no longer advertises the Darwin/AAPCS64 sharing detail in its doc comment; that note now lives inside the function, next to the loop it explains.
  • The IdentityInt64AndDouble round-trip is no longer arm64-only: the plain case is meaningful on every architecture this test file covers (verified on linux/amd64 and under qemu on linux/arm64, ppc64le and loong64), so it now runs unconditionally. Only the IdentityInt64AndDoubleAfterRegisters spill case stays arm64-gated, since it asserts the eightbyte-count-based integer register exhaustion that is specific to AAPCS64/Darwin.

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 struct tests run under qemu-aarch64 against a real cross-compiled C library, and I re-confirmed that IdentityInt64AndDouble still fails (B comes back as 0) when the integer-register packing path is removed.

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.
…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.
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.
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.
…test

Collapse the AAPCS64 rationale in addStruct and placeRegistersArm64 to
short notes and move the cross-ABI sharing detail of
copyStruct8ByteChunks into the function body. The plain
struct{int64; double} round-trip is not arm64-specific: every ABI
covered by this test places each eightbyte in the register its class
selects and purego already does that, so run it on all platforms and
keep only the register-overflow variant arm64-only.
@kumagi
kumagi force-pushed the fix/arm64-mixed-struct branch from 9044883 to 4cedc11 Compare September 17, 2026 18:06
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

4 participants