diff --git a/export_test.go b/export_test.go new file mode 100644 index 00000000..09ff423e --- /dev/null +++ b/export_test.go @@ -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) +} diff --git a/func.go b/func.go index 3ba44bbc..97764cd7 100644 --- a/func.go +++ b/func.go @@ -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++ + } } } @@ -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()) @@ -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 } diff --git a/func_test.go b/func_test.go index 970919bb..65c9acb9 100644 --- a/func_test.go +++ b/func_test.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "runtime" "strings" "sync" @@ -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 diff --git a/struct_amd64.go b/struct_amd64.go index ab2e2a01..7d03ebe7 100644 --- a/struct_amd64.go +++ b/struct_amd64.go @@ -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() diff --git a/struct_arm.go b/struct_arm.go index bfaac841..9abad17c 100644 --- a/struct_arm.go +++ b/struct_arm.go @@ -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 { diff --git a/struct_arm64.go b/struct_arm64.go index 47e6f452..aa12f96f 100644 --- a/struct_arm64.go +++ b/struct_arm64.go @@ -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 { diff --git a/struct_loong64.go b/struct_loong64.go index ad425441..63564789 100644 --- a/struct_loong64.go +++ b/struct_loong64.go @@ -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 { diff --git a/struct_other.go b/struct_other.go index 1306a235..19520326 100644 --- a/struct_other.go +++ b/struct_other.go @@ -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 { diff --git a/struct_ppc64le.go b/struct_ppc64le.go index 0cb481e4..df911c11 100644 --- a/struct_ppc64le.go +++ b/struct_ppc64le.go @@ -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() diff --git a/struct_riscv64.go b/struct_riscv64.go index aa4e50b6..c2534ef0 100644 --- a/struct_riscv64.go +++ b/struct_riscv64.go @@ -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() diff --git a/struct_s390x.go b/struct_s390x.go index 6e15d415..a3e250e3 100644 --- a/struct_s390x.go +++ b/struct_s390x.go @@ -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()