Skip to content

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

Description

@kumagi

PureGo Version

main @ 763bb99

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • NetBSD
  • Android
  • iOS

Go Version (go version)

go1.26.7 linux/amd64 (finding is in arm64-only code; verified by code inspection + cross-build, see below)

What steps will reproduce the problem?

On arm64, placeRegistersArm64 (struct_arm64.go, the non-darwin path) routes struct members straight to FP or integer registers by kind:

case reflect.Int64, reflect.Int:
    addInt(uintptr(f.Int()))       // GPR
...
case reflect.Float64:
    addFloat(...)                  // FPR

So a non-HFA mixed struct such as {int64; float64} is sent as x0 + v0. But AAPCS64 says a non-HFA composite of 16 bytes or less is packed into 1–2 general-purpose registers (x0, x1) — and purego's own receive paths already do exactly that:

  • getCallbackStruct (struct_arm64.go:595-611): "Non-HFA composite ≤ 16 bytes: packed into 1–2 integer registers", reading only f[numOfFloatRegisters()+*intsN ...].
  • getStruct (struct_arm64.go:38-55): non-isAllSameFloat returns fall back to syscall.a1, syscall.a2.

Three code paths, one of which disagrees — the sender is the odd one out (3-against-1 within the repo, plus the ABI spec).

Repro (arm64 only — this box is amd64/Linux, so no execution trace; the case below is what to run on an M-series Mac, Linux/arm64, or Android/iOS device):

struct Mixed { int64_t i; double d; };
struct Mixed ident_mixed(struct Mixed s) { return s; }
type Mixed struct {
    _ structs.HostLayout
    I int64
    D float64
}
var ident func(Mixed) Mixed
purego.RegisterLibFunc(&ident, lib, "ident_mixed")
got := ident(Mixed{I: 1, D: 2.5})
// want {I:1 D:2.5}; with the bug D arrives in v0 while C reads x1

Additionally a Go→C→Go round trip through NewCallback is self-inconsistent even without C: the sender writes x0/v0 while the receiver reads x0/x1.

What is the expected result?

Mixed non-HFA structs travel packed in x0/x1, matching AAPCS64 and purego's own receive paths, so the round trip above returns {I:1 D:2.5}.

What happens instead?

The float member goes out on v0 while the callee (real C ABI, or purego's own callback receiver) reads x1: the double arrives as garbage/zero and integer register accounting shifts for all following arguments.

Anything else you feel useful to add?

Minimized reproducible case: the ident_mixed C helper plus the Go snippet above. Requires an arm64 host (or device) to execute; on this amd64 box I verified instead that (1) GOOS=linux GOARCH=arm64 and GOOS=darwin GOARCH=arm64 builds pass, (2) the existing test suite passes, and (3) the three in-repo code paths cited above disagree exactly as described.

Related PR: #517.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions