Skip to content

purego: fix struct-return stack-argument undercount#476

Merged
hajimehoshi merged 1 commit into
ebitengine:mainfrom
hajimehoshi:claude/consolidate-struct-return-7c9bb6
Jul 13, 2026
Merged

purego: fix struct-return stack-argument undercount#476
hajimehoshi merged 1 commit into
ebitengine:mainfrom
hajimehoshi:claude/consolidate-struct-return-7c9bb6

Conversation

@hajimehoshi

@hajimehoshi hajimehoshi commented Jul 12, 2026

Copy link
Copy Markdown
Member

What issue is this addressing?

Closes #477

What type of issue is this addressing?

bug + refactor

What this PR does | solves

Bug fix (#477)

The preliminary argument count in RegisterFunc reserved a register for the
hidden struct-return pointer with a bare ints++. That fails to model the
spill: when the integer registers are already full, the hidden pointer is
prepended as the first integer argument and pushes a regular argument onto the
stack, so a stack slot must be counted instead. Without it, the
stack > sizeOfStack guard can pass while the real call writes one past the
end of the fixed-size sysargs array. The count now spills the same way an
ordinary integer argument does.

Scope: pre-existing on amd64 and reachable on amd64 and loong64. Windows/amd64
was unaffected (its guard uses the ints+floats+stack sum) and arm64 is
unaffected (it returns the pointer in the dedicated R8 register).

Refactor

func.go decided how a struct return value is passed by inspecting
runtime.GOARCH at two call sites in RegisterFunc, via the
amd64StructReturnInMemory helper plus explicit amd64 / loong64 /
ppc64le / riscv64 / s390x branches.

This replaces that with a per-architecture structReturnInMemory(size uintptr) bool
function, defined once in each struct_<arch>.go file and selected by the
filename build tag — the same dispatch mechanism already used for
getStruct, addStruct, and setStruct. The two struct-return call sites in
RegisterFunc now just call structReturnInMemory(...) with no GOARCH
checks.

Per-arch behavior of the new predicate ("is the struct returned via a
caller-allocated hidden pointer passed as the first integer argument?"):

  • amd64: the previous Win64 + System V logic, moved verbatim.
  • loong64 / ppc64le / riscv64 / s390x: size > maxRegAllocStructSize.
  • arm64: always false — arm64 returns the pointer through the dedicated
    R8 indirect-result register, not an ordinary argument, so that case remains a
    separate branch.
  • arm / other (structs unsupported): always false.

Test

Adds TestABI_StructReturnHiddenPointer, which registers maxArgs integer
arguments plus a large struct return and asserts the registration is rejected.
It fails on the pre-fix counting and passes after the fix; it self-gates so it
skips on architectures that do not route the return through a hidden integer
argument.

Testing

  • gofmt -s clean; go vet clean.
  • Builds clean for linux amd64/arm64/loong64/ppc64le/riscv64/arm/386, windows
    amd64/arm64, darwin amd64/arm64. (linux/s390x's pre-existing cgo/assembly
    build requirement is unaffected by this change.)
  • Full test suite passes on darwin/arm64 and darwin/amd64 (Rosetta).

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

@hajimehoshi

Copy link
Copy Markdown
Member Author

This is a pure refactoring, and this should be a good change itself

Please take a look @TotallyGamerJet

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.

Pull request overview

Refactors struct-return ABI handling by moving the “struct returned via hidden first integer argument” predicate into per-architecture files selected by filename/build constraints, removing GOARCH branching from RegisterFunc.

Changes:

  • Introduces structReturnInMemory(size uintptr) bool in each struct_<arch>.go implementation and removes amd64StructReturnInMemory from func.go.
  • Updates RegisterFunc to use structReturnInMemory(...) for both preflight argument counting and runtime call setup.
  • Keeps arm64’s indirect-result (R8) handling as a separate path by making structReturnInMemory always false on arm64.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
func.go Replaces per-GOARCH struct-return handling with structReturnInMemory(...) and removes the old amd64-only helper.
struct_amd64.go Adds amd64-specific structReturnInMemory (Win64 + SysV logic) and switches getStruct to use it.
struct_arm64.go Adds arm64 structReturnInMemory stub returning false to keep R8 indirect-result handling separate.
struct_arm.go Adds arm structReturnInMemory stub returning false.
struct_loong64.go Adds loong64 structReturnInMemory predicate based on maxRegAllocStructSize.
struct_ppc64le.go Adds ppc64le structReturnInMemory predicate based on maxRegAllocStructSize.
struct_riscv64.go Adds riscv64 structReturnInMemory predicate based on maxRegAllocStructSize.
struct_s390x.go Adds s390x structReturnInMemory predicate based on maxRegAllocStructSize.
struct_other.go Adds fallback structReturnInMemory stub returning false for unsupported architectures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread func.go
Comment thread struct_arm.go Outdated
@hajimehoshi

Copy link
Copy Markdown
Member Author

This is no longer a pure refactoring but a bug fix for #477

RegisterFunc's preliminary argument count reserved a register for the
hidden struct-return pointer with a bare ints++. That fails to model the
spill: when the integer registers are already full, the pointer is
prepended as the first integer argument and pushes a regular argument
onto the stack, so a stack slot must be counted instead. Without it, the
"too many stack arguments" guard can pass while the actual call writes
one past the end of the fixed-size sysargs array. The count now spills
the same way an ordinary integer argument does.

The bug was pre-existing on amd64 and reachable on amd64 and loong64.
Windows/amd64 was unaffected because its guard uses the ints+floats+stack
sum, and arm64 is unaffected because it returns the pointer in the
dedicated R8 register.

This also consolidates how the struct-return convention is selected.
Replace the amd64StructReturnInMemory helper in func.go, and the
runtime.GOARCH branches that guarded it, with a per-architecture
structReturnInMemory function defined once in each struct_<arch>.go file
and selected by the filename build tag, matching how getStruct,
addStruct, and setStruct are already dispatched. arm64 keeps a separate
branch because it uses R8 rather than an ordinary argument.

Add TestABI_StructReturnHiddenPointer, which registers maxArgs integer
arguments plus a large struct return and asserts that the registration
is rejected.

Closes ebitengine#477

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@hajimehoshi
hajimehoshi force-pushed the claude/consolidate-struct-return-7c9bb6 branch from edb6ac5 to c279735 Compare July 12, 2026 14:30
@hajimehoshi hajimehoshi changed the title purego: make struct return-in-memory a per-arch function purego: fix struct-return stack-argument undercount Jul 12, 2026

@TotallyGamerJet TotallyGamerJet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@hajimehoshi
hajimehoshi merged commit ca3432a into ebitengine:main Jul 13, 2026
25 checks passed
@hajimehoshi
hajimehoshi deleted the claude/consolidate-struct-return-7c9bb6 branch July 13, 2026 01:53
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.

RegisterFunc: hidden struct-return pointer undercounts stack arguments on amd64/loong64

3 participants