Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 The Ebitengine Authors

//go:build darwin || freebsd || linux || netbsd || windows

package purego

// MaxArgs re-exports maxArgs for external tests.
const MaxArgs = maxArgs

// StructReturnInMemory re-exports structReturnInMemory for external tests.
func StructReturnInMemory(size uintptr) bool {
return structReturnInMemory(size)
}
42 changes: 13 additions & 29 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,16 @@ func RegisterFunc(fptr any, cfn uintptr) {
ensureStructSupported()
outType := ty.Out(0)
checkStructFieldsSupported(outType)
if runtime.GOARCH == "amd64" && amd64StructReturnInMemory(outType.Size()) {
// on amd64 a struct returned in memory is allocated by the caller
// and its pointer is passed as a hidden first argument.
ints++
if structReturnInMemory(outType.Size()) {
// A struct returned in memory is allocated by the caller and its
// pointer is passed as a hidden first integer argument. When the
// integer registers are already full, prepending it spills a
// regular argument onto the stack.
if ints < numOfIntegerRegisters() {
ints++
} else {
stack++
}
}
}

Expand Down Expand Up @@ -286,9 +292,9 @@ func RegisterFunc(fptr any, cfn uintptr) {
var arm64_r8 uintptr
if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
outType := ty.Out(0)
amd64InMemory := runtime.GOARCH == "amd64" && amd64StructReturnInMemory(outType.Size())
otherInMemory := (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize
if amd64InMemory || otherInMemory {
if structReturnInMemory(outType.Size()) {
// The caller allocates the return value and passes its pointer
// as a hidden first integer argument.
val := reflect.New(outType)
keepAlive = append(keepAlive, val)
addInt(val.Pointer())
Expand Down Expand Up @@ -533,28 +539,6 @@ func ensureCallbackStructSupported() {
// iOS (GOOS=ios) shares it with macOS (GOOS=darwin).
const isDarwin = runtime.GOOS == "darwin" || runtime.GOOS == "ios"

// amd64StructReturnInMemory reports whether a struct return value of the given
// size is returned through a caller-allocated hidden pointer (true) rather than
// in registers (false). It must only be consulted on amd64.
func amd64StructReturnInMemory(size uintptr) bool {
if size == 0 {
return false
}
if runtime.GOOS == "windows" {
// The Win64 ABI returns aggregates of exactly 1, 2, 4, or 8 bytes in
// RAX. Every other size is returned through a caller-allocated hidden
// pointer that the callee also returns in RAX.
switch size {
case 1, 2, 4, 8:
return false
default:
return true
}
}
// The System V ABI returns aggregates of up to two eightbytes in registers.
return size > maxRegAllocStructSize
}

func roundUpTo8(val uintptr) uintptr {
return (val + align8ByteMask) &^ align8ByteMask
}
Expand Down
36 changes: 36 additions & 0 deletions func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strings"
"sync"
Expand Down Expand Up @@ -597,6 +598,41 @@ func TestABI_TooManyArguments(t *testing.T) {
})
}

func TestABI_StructReturnHiddenPointer(t *testing.T) {
// A struct returned in memory is passed a hidden pointer as the first
// integer argument, so maxArgs integer arguments plus it need one more slot
// than sysargs holds and RegisterFunc must reject the registration.
type bigStruct struct{ A, B, C uint64 } // larger than two eightbytes

if !purego.StructReturnInMemory(reflect.TypeFor[bigStruct]().Size()) {
t.Skipf("GOARCH=%s does not return large structs via a hidden integer argument", runtime.GOARCH)
}

in := make([]reflect.Type, purego.MaxArgs)
for i := range in {
in[i] = reflect.TypeFor[uintptr]()
}
fnType := reflect.FuncOf(in, []reflect.Type{reflect.TypeFor[bigStruct]()}, false)
fptr := reflect.New(fnType)

defer func() {
switch r := recover(); {
case r == nil:
t.Fatal("RegisterFunc accepted the call; the hidden struct-return pointer was not counted against the stack limit")
case strings.Contains(fmt.Sprint(r), "too many stack arguments"):
// Expected: the guard fired.
case strings.Contains(fmt.Sprint(r), "only supported on"):
t.Skipf("struct returns are unsupported on this platform: %v", r)
default:
t.Fatalf("unexpected panic: %v", r)
}
}()

// A non-zero cfn passes the nil check; the panic fires during the preflight
// argument count, before the function is ever called.
purego.RegisterFunc(fptr.Interface(), uintptr(1))
}

func buildSharedLib(tb testing.TB, compilerEnv, libFile string, sources ...string) error {
tb.Helper()
// When PUREGO_TEST_PREBUILT_LIBDIR is set, the shared library has been
Expand Down
24 changes: 23 additions & 1 deletion struct_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,35 @@ import (
"unsafe"
)

// structReturnInMemory reports whether a struct return value of the given size
// is returned through a caller-allocated hidden pointer passed as the first
// integer argument (true) rather than in registers (false).
func structReturnInMemory(size uintptr) bool {
if size == 0 {
return false
}
if runtime.GOOS == "windows" {
// The Win64 ABI returns aggregates of exactly 1, 2, 4, or 8 bytes in
// RAX. Every other size is returned through a caller-allocated hidden
// pointer that the callee also returns in RAX.
switch size {
case 1, 2, 4, 8:
return false
default:
return true
}
}
// The System V ABI returns aggregates of up to two eightbytes in registers.
return size > maxRegAllocStructSize
}

func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) {
outSize := outType.Size()
if runtime.GOOS == "windows" {
switch {
case outSize == 0:
return reflect.New(outType).Elem()
case amd64StructReturnInMemory(outSize):
case structReturnInMemory(outSize):
// Returned through the caller-allocated hidden pointer, which the
// callee also returns in RAX.
return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem()
Expand Down
7 changes: 7 additions & 0 deletions struct_arm.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
return keepAlive
}

// structReturnInMemory always reports false on arm: an indirect struct return
// is recovered from the pointer the callee leaves in a1 (see getStruct) rather
// than through a caller-allocated hidden first argument.
func structReturnInMemory(size uintptr) bool {
return false
}

func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) {
outSize := outType.Size()
if outSize == 0 {
Expand Down
7 changes: 7 additions & 0 deletions struct_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import (
"github.com/ebitengine/purego/internal/strings"
)

// structReturnInMemory always reports false on arm64: a struct returned in
// memory is passed through the dedicated indirect result register (R8), not as
// an ordinary integer argument, so it is handled separately.
func structReturnInMemory(size uintptr) bool {
return false
}

func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) {
outSize := outType.Size()
switch {
Expand Down
7 changes: 7 additions & 0 deletions struct_loong64.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ func loong64Classify(t reflect.Type) (leaves []loong64Leaf, useFP bool) {
}
}

// structReturnInMemory reports whether a struct return value of the given size
// is returned through a caller-allocated hidden pointer passed as the first
// integer argument. Aggregates larger than two eightbytes are returned in memory.
func structReturnInMemory(size uintptr) bool {
return size > maxRegAllocStructSize
}

func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
outSize := outType.Size()
if outSize == 0 {
Expand Down
7 changes: 7 additions & 0 deletions struct_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
panic("purego: struct returns are not supported on this architecture")
}

// structReturnInMemory reports whether a struct return value of the given size
// is returned through a caller-allocated hidden pointer. Structs are unsupported
// on this architecture, so it always reports false.
func structReturnInMemory(size uintptr) bool {
return false
}

// shouldBundleStackArgs always returns false because C-style stack argument
// bundling is only needed on Darwin ARM64.
func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
Expand Down
7 changes: 7 additions & 0 deletions struct_ppc64le.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import (
"unsafe"
)

// structReturnInMemory reports whether a struct return value of the given size
// is returned through a caller-allocated hidden pointer passed as the first
// integer argument. Aggregates larger than two eightbytes are returned in memory.
func structReturnInMemory(size uintptr) bool {
return size > maxRegAllocStructSize
}

func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
outSize := outType.Size()

Expand Down
7 changes: 7 additions & 0 deletions struct_riscv64.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import (
"unsafe"
)

// structReturnInMemory reports whether a struct return value of the given size
// is returned through a caller-allocated hidden pointer passed as the first
// integer argument. Aggregates larger than two eightbytes are returned in memory.
func structReturnInMemory(size uintptr) bool {
return size > maxRegAllocStructSize
}

func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
outSize := outType.Size()

Expand Down
7 changes: 7 additions & 0 deletions struct_s390x.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import (
"unsafe"
)

// structReturnInMemory reports whether a struct return value of the given size
// is returned through a caller-allocated hidden pointer passed as the first
// integer argument. Aggregates larger than two eightbytes are returned in memory.
func structReturnInMemory(size uintptr) bool {
return size > maxRegAllocStructSize
}

func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
outSize := outType.Size()

Expand Down
Loading