diff --git a/README.md b/README.md index d431def1..a3721ff9 100644 --- a/README.md +++ b/README.md @@ -30,28 +30,30 @@ except for float arguments and return values. Tier 1 platforms are the primary targets officially supported by PureGo. When a new version of PureGo is released, any critical bugs found on Tier 1 platforms are treated as release blockers. The release will be postponed until such issues are resolved. -- **Android**: amd641, arm641 -- **iOS**: amd641, arm641 +- **Android**: amd641,2, arm641,2 +- **iOS**: amd641,2, arm641,2 - **Linux**: amd64, arm64 - **macOS**: amd64, arm64 -- **Windows**: amd64, arm64 +- **Windows**: amd643, arm643 ### Tier 2 Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs on Tier 2 platforms do not block new PureGo releases. However, fixes contributed by external contributors are very welcome and encouraged. -- **Android**: 3861, arm1 -- **FreeBSD**: amd642, arm642 -- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x1 -- **NetBSD**: amd642, arm642 -- **Windows**: 3863, arm3,4 +- **Android**: 3861,2, arm1,2 +- **FreeBSD**: amd642,4, arm642,4 +- **Linux**: 3862, arm2, loong642, ppc64le2, riscv642, s390x1,2 +- **NetBSD**: amd642,4, arm642,4 +- **Windows**: 3862,5, arm2,5,6 #### Support Notes 1. These architectures require CGO_ENABLED=1 to compile -2. These architectures require the special flag `-gcflags="github.com/ebitengine/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0 -3. These architectures only support `SyscallN` and `NewCallback` -4. These architectures are no longer supported as of Go 1.26 +2. These architectures do not support passing structs by value as arguments or return values +3. These architectures support passing structs by value as arguments and return values when calling C functions, but not in callbacks created with `NewCallback` +4. These architectures require the special flag `-gcflags="github.com/ebitengine/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0 +5. These architectures only support `SyscallN` and `NewCallback` +6. These architectures are no longer supported as of Go 1.26 ## Example diff --git a/func.go b/func.go index 11df76dd..889be836 100644 --- a/func.go +++ b/func.go @@ -64,7 +64,7 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { // int64 <=> int64_t // float32 <=> float // float64 <=> double -// struct <=> struct (darwin amd64/arm64, linux amd64/arm64) +// struct <=> struct (darwin amd64/arm64, linux amd64/arm64, windows amd64/arm64) // func <=> C function // unsafe.Pointer, *T <=> void* // []T => void* @@ -102,6 +102,9 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { // On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack, // following the C ABI's byte-level packing rules. // +// On Windows, struct arguments and returns are supported on amd64 and arm64 when calling C functions. +// Passing or returning structs in callbacks created with [NewCallback] is not supported on Windows. +// // # Example // // All functions below call this C function: @@ -175,7 +178,8 @@ func RegisterFunc(fptr any, cfn uintptr) { } case reflect.Struct: ensureStructSupported() - if arg.Size() == 0 { + if arg.Size() == 0 && runtime.GOOS != "windows" { + // On Windows an empty struct still consumes one argument slot. continue } addInt := func(u uintptr) { @@ -196,9 +200,9 @@ func RegisterFunc(fptr any, cfn uintptr) { ensureStructSupported() outType := ty.Out(0) checkStructFieldsSupported(outType) - if runtime.GOARCH == "amd64" && outType.Size() > maxRegAllocStructSize { - // on amd64 if struct is bigger than 16 bytes allocate the return struct - // and pass it in as a hidden first argument. + 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++ } } @@ -283,7 +287,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) - if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize { + 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 { val := reflect.New(outType) keepAlive = append(keepAlive, val) addInt(val.Pointer()) @@ -499,9 +505,31 @@ func ensureStructSupported() { if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { panic("purego: struct arguments/returns are only supported on amd64 and arm64") } - if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { - panic("purego: struct arguments/returns are only supported on darwin and linux") + if runtime.GOOS != "darwin" && runtime.GOOS != "linux" && runtime.GOOS != "windows" { + panic("purego: struct arguments/returns are only supported on darwin, linux, and windows") + } +} + +// 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 { diff --git a/struct_amd64.go b/struct_amd64.go index 40ed91c0..da7044c0 100644 --- a/struct_amd64.go +++ b/struct_amd64.go @@ -12,6 +12,19 @@ import ( 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): + // 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() + default: + // 1, 2, 4, or 8 byte aggregates are returned in RAX. + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem() + } + } switch { case outSize == 0: return reflect.New(outType).Elem() @@ -87,6 +100,12 @@ const ( ) func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { + if runtime.GOOS == "windows" { + // Win64 still passes an empty struct as an argument slot, so this must + // run before the zero-size early return used by the System V path. + return addStructWindows(v, addInt, keepAlive) + } + if v.Type().Size() == 0 { return keepAlive } @@ -112,6 +131,27 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl return keepAlive } +// addStructWindows passes a struct argument under the Win64 ABI. Aggregates of +// exactly 1, 2, 4, or 8 bytes are passed by value in a single integer slot; all +// other sizes are passed as a pointer to a caller-allocated copy. Empty structs +// fall in the latter group: unlike the System V ABI, Win64 still consumes an +// argument slot for them. +func addStructWindows(v reflect.Value, addInt func(uintptr), keepAlive []any) []any { + switch v.Type().Size() { + case 1, 2, 4, 8: + var val uintptr + reflect.NewAt(v.Type(), unsafe.Pointer(&val)).Elem().Set(v) + addInt(val) + default: + ptrStruct := reflect.New(v.Type()) + ptrStruct.Elem().Set(v) + ptr := ptrStruct.Elem().Addr().UnsafePointer() + keepAlive = append(keepAlive, ptr) + addInt(uintptr(ptr)) + } + return keepAlive +} + func postMerger(t reflect.Type) (passInMemory bool) { // (c) If the size of the aggregate exceeds two eightbytes and the first eight- byte isn’t SSE or any other // eightbyte isn’t SSEUP, the whole argument is passed in memory. diff --git a/struct_test.go b/struct_test.go index 97428e2f..1cd30d39 100644 --- a/struct_test.go +++ b/struct_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors -//go:build (darwin || linux) && (amd64 || arm64) +//go:build (darwin || linux || windows) && (amd64 || arm64) package purego_test @@ -15,6 +15,7 @@ import ( "unsafe" "github.com/ebitengine/purego" + "github.com/ebitengine/purego/internal/load" ) func TestRegisterFunc_structArgs(t *testing.T) { @@ -26,10 +27,15 @@ func TestRegisterFunc_structArgs(t *testing.T) { } defer os.Remove(libFileName) - lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL) + lib, err := load.OpenLibrary(libFileName) if err != nil { - t.Fatalf("Dlopen(%q) failed: %v", libFileName, err) + t.Fatalf("OpenLibrary(%q) failed: %v", libFileName, err) } + defer func() { + if err := load.CloseLibrary(lib); err != nil { + t.Fatalf("failed to close library: %v", err) + } + }() const ( expectedUnsigned = 0xdeadbeef @@ -41,8 +47,9 @@ func TestRegisterFunc_structArgs(t *testing.T) { ) implementations := []struct { - name string - register func(fptr any, handle uintptr, name string, goFn any) + name string + usesCallbacks bool + register func(fptr any, handle uintptr, name string, goFn any) }{ { name: "RegisterLibFunc", @@ -51,7 +58,8 @@ func TestRegisterFunc_structArgs(t *testing.T) { }, }, { - name: "GoCallbackFunc", + name: "GoCallbackFunc", + usesCallbacks: true, register: func(fptr any, handle uintptr, name string, goFn any) { fnType := reflect.TypeOf(fptr).Elem() if fnType.NumOut() > 0 { @@ -69,6 +77,11 @@ func TestRegisterFunc_structArgs(t *testing.T) { } for _, imp := range implementations { imp := imp + if imp.usesCallbacks && runtime.GOOS == "windows" { + // Callbacks on Windows use the stdlib syscall.NewCallback, which does + // not support struct arguments or returns. + continue + } t.Run(imp.name, func(t *testing.T) { register := imp.register { @@ -865,20 +878,27 @@ func TestRegisterFunc_structReturns(t *testing.T) { } defer os.Remove(libFileName) - lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL) + lib, err := load.OpenLibrary(libFileName) if err != nil { - t.Fatalf("Dlopen(%q) failed: %v", libFileName, err) + t.Fatalf("OpenLibrary(%q) failed: %v", libFileName, err) } + defer func() { + if err := load.CloseLibrary(lib); err != nil { + t.Fatalf("failed to close library: %v", err) + } + }() implementations := []struct { - name string - register func(fptr any, handle uintptr, name string) + name string + usesCallbacks bool + register func(fptr any, handle uintptr, name string) }{ { name: "RegisterLibFunc", register: purego.RegisterLibFunc, }, { - name: "GoCallbackFunc", + name: "GoCallbackFunc", + usesCallbacks: true, register: func(fptr any, _ uintptr, _ string) { fn := reflect.MakeFunc(reflect.TypeOf(fptr).Elem(), func(args []reflect.Value) []reflect.Value { retType := reflect.TypeOf(fptr).Elem().Out(0) @@ -899,6 +919,11 @@ func TestRegisterFunc_structReturns(t *testing.T) { } for _, imp := range implementations { imp := imp + if imp.usesCallbacks && runtime.GOOS == "windows" { + // Callbacks on Windows use the stdlib syscall.NewCallback, which does + // not support struct arguments or returns. + continue + } t.Run(imp.name, func(t *testing.T) { register := imp.register { diff --git a/testdata/structtest/struct_test.c b/testdata/structtest/struct_test.c index c6be0a69..b513b816 100644 --- a/testdata/structtest/struct_test.c +++ b/testdata/structtest/struct_test.c @@ -5,16 +5,11 @@ #include #include -#if defined(__x86_64__) || defined(__aarch64__) -typedef int64_t GoInt; -typedef uint64_t GoUint; -#endif - // Empty is empty struct Empty {}; // NoStruct tests that an empty struct doesn't cause issues -unsigned long NoStruct(struct Empty e) { +uint64_t NoStruct(struct Empty e) { return 0xdeadbeef; } @@ -22,28 +17,28 @@ struct EmptyEmpty { struct Empty x; }; -unsigned long EmptyEmpty(struct EmptyEmpty e) { +uint64_t EmptyEmpty(struct EmptyEmpty e) { return 0xdeadbeef; } -unsigned long EmptyEmptyWithReg(unsigned int x, struct EmptyEmpty e, unsigned int y) { +uint64_t EmptyEmptyWithReg(unsigned int x, struct EmptyEmpty e, unsigned int y) { return (x << 16) | y; } // GreaterThan16Bytes is 24 bytes on 64 bit systems struct GreaterThan16Bytes { - long *x, *y, *z; + int64_t *x, *y, *z; }; // GreaterThan16Bytes is a basic test for structs bigger than 16 bytes -unsigned long GreaterThan16Bytes(struct GreaterThan16Bytes g) { +uint64_t GreaterThan16Bytes(struct GreaterThan16Bytes g) { return *g.x + *g.y + *g.z; } // AfterRegisters tests to make sure that structs placed on the stack work properly -unsigned long AfterRegisters(long a, long b, long c, long d, long e, long f, long g, long h, struct GreaterThan16Bytes bytes) { - long registers = a + b + c + d + e + f + g + h; - long stack = *bytes.x + *bytes.y + *bytes.z; +uint64_t AfterRegisters(intptr_t a, intptr_t b, intptr_t c, intptr_t d, intptr_t e, intptr_t f, intptr_t g, intptr_t h, struct GreaterThan16Bytes bytes) { + intptr_t registers = a + b + c + d + e + f + g + h; + int64_t stack = *bytes.x + *bytes.y + *bytes.z; if (registers != stack) { return 0xbadbad; } @@ -53,25 +48,25 @@ unsigned long AfterRegisters(long a, long b, long c, long d, long e, long f, lon return stack; } -unsigned long BeforeRegisters(struct GreaterThan16Bytes bytes, long a, long b) { +uint64_t BeforeRegisters(struct GreaterThan16Bytes bytes, int64_t a, int64_t b) { return *bytes.x + *bytes.y + *bytes.z + a + b; } struct GreaterThan16BytesStruct { struct { - long *x, *y, *z; + int64_t *x, *y, *z; } a ; }; -unsigned long GreaterThan16BytesStruct(struct GreaterThan16BytesStruct g) { +uint64_t GreaterThan16BytesStruct(struct GreaterThan16BytesStruct g) { return *(g.a.x) + *(g.a.y) + *(g.a.z); } struct IntLessThan16Bytes { - long x, y; + int64_t x, y; }; -unsigned long IntLessThan16Bytes(struct IntLessThan16Bytes l) { +uint64_t IntLessThan16Bytes(struct IntLessThan16Bytes l) { return l.x + l.y; } @@ -201,23 +196,23 @@ struct Short { unsigned short a, b, c, d; }; -unsigned long Short(struct Short s) { - return (long)s.a << 48 | (long)s.b << 32 | (long)s.c << 16 | (long)s.d << 0; +uint64_t Short(struct Short s) { + return (uint64_t)s.a << 48 | (uint64_t)s.b << 32 | (uint64_t)s.c << 16 | (uint64_t)s.d << 0; } struct Int { unsigned int a, b; }; -unsigned long Int(struct Int i) { - return (long)i.a << 32 | (long)i.b << 0; +uint64_t Int(struct Int i) { + return (uint64_t)i.a << 32 | (uint64_t)i.b << 0; } struct Long { - unsigned long a; + uint64_t a; }; -unsigned long Long(struct Long l) { +uint64_t Long(struct Long l) { return l.a; } @@ -327,31 +322,31 @@ struct Content { struct { double width, height; } size; }; -unsigned long InitWithContentRect(int *win, struct Content c, int style, int backing, _Bool flag) { +uint64_t InitWithContentRect(int *win, struct Content c, int style, int backing, _Bool flag) { if (win == 0) return 0xBAD; if (!flag) return 0xF1A6; // FLAG - return (unsigned long)(c.point.x + c.point.y + c.size.width + c.size.height) / (style - backing); + return (uint64_t)(c.point.x + c.point.y + c.size.width + c.size.height) / (style - backing); } struct GoInt4 { - GoInt a, b, c, d; + intptr_t a, b, c, d; }; -GoInt GoInt4(struct GoInt4 g) { +intptr_t GoInt4(struct GoInt4 g) { return g.a + g.b - g.c + g.d; } struct GoUint4 { - GoUint a, b, c, d; + uintptr_t a, b, c, d; }; -GoUint GoUint4(struct GoUint4 g) { +uintptr_t GoUint4(struct GoUint4 g) { return g.a + g.b + g.c + g.d; } -GoUint TakeGoUintAndReturn(GoUint a) { +uintptr_t TakeGoUintAndReturn(uintptr_t a) { return a; } @@ -395,7 +390,7 @@ uintptr_t AddPointers(struct TwoPointers wrapper) { // Identity functions for round-trip testing of struct arguments struct OneInt64 { - long long a; + int64_t a; }; struct OneInt64 IdentityOneInt64(struct OneInt64 s) { @@ -423,7 +418,7 @@ struct FloatAndInt IdentityFloatAndInt(struct FloatAndInt s) { } struct ThreeInt64 { - long long a, b, c; + int64_t a, b, c; }; struct ThreeInt64 IdentityThreeInt64(struct ThreeInt64 s) { @@ -431,16 +426,16 @@ struct ThreeInt64 IdentityThreeInt64(struct ThreeInt64 s) { } struct PtrInt64Ptr { - long long *a; - long long b; - long long *c; + int64_t *a; + int64_t b; + int64_t *c; }; struct PtrInt64Ptr IdentityPtrInt64Ptr(struct PtrInt64Ptr s) { return s; } -struct IntLessThan16Bytes IdentityTwoInt64AfterPrims(long long x, double y, struct IntLessThan16Bytes s) { +struct IntLessThan16Bytes IdentityTwoInt64AfterPrims(int64_t x, double y, struct IntLessThan16Bytes s) { return s; } @@ -449,7 +444,7 @@ struct FloatLessThan16Bytes IdentityTwoFloat32AfterFloats(double x, double y, st } struct Mixed5Args { - long long *a; + int64_t *a; int32_t b; float c; int32_t d;