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;