Skip to content
Open
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
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**: amd64<sup>1</sup>, arm64<sup>1</sup>
- **iOS**: amd64<sup>1</sup>, arm64<sup>1</sup>
- **Android**: amd64<sup>1,2</sup>, arm64<sup>1,2</sup>
- **iOS**: amd64<sup>1,2</sup>, arm64<sup>1,2</sup>
- **Linux**: amd64, arm64
- **macOS**: amd64, arm64
- **Windows**: amd64, arm64
- **Windows**: amd64<sup>3</sup>, arm64<sup>3</sup>

### 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**: 386<sup>1</sup>, arm<sup>1</sup>
- **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x<sup>1</sup>
- **NetBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
- **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>
- **Android**: 386<sup>1,2</sup>, arm<sup>1,2</sup>
- **FreeBSD**: amd64<sup>2,4</sup>, arm64<sup>2,4</sup>
- **Linux**: 386<sup>2</sup>, arm<sup>2</sup>, loong64<sup>2</sup>, ppc64le<sup>2</sup>, riscv64<sup>2</sup>, s390x<sup>1,2</sup>
- **NetBSD**: amd64<sup>2,4</sup>, arm64<sup>2,4</sup>
- **Windows**: 386<sup>2,5</sup>, arm<sup>2,5,6</sup>

#### 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

Expand Down
44 changes: 36 additions & 8 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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) {
Expand All @@ -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++
}
}
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions struct_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Expand All @@ -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))
}
Comment thread
hajimehoshi marked this conversation as resolved.
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.
Expand Down
47 changes: 36 additions & 11 deletions struct_test.go
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -15,6 +15,7 @@ import (
"unsafe"

"github.com/ebitengine/purego"
"github.com/ebitengine/purego/internal/load"
)

func TestRegisterFunc_structArgs(t *testing.T) {
Expand All @@ -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
Expand All @@ -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",
Expand All @@ -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 {
Expand All @@ -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
{
Expand Down Expand Up @@ -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)
Expand All @@ -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
{
Expand Down
Loading