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
11 changes: 11 additions & 0 deletions cdecl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 The Ebitengine Authors

package purego

// CDecl marks a function as being called using the __cdecl calling convention as defined in
// the [MSDocs] when passed to NewCallback. It must be the first argument to the function.
// This is only useful on 386 Windows, but it is safe to use on other platforms.
//
// [MSDocs]: https://learn.microsoft.com/en-us/cpp/cpp/cdecl?view=msvc-170
type CDecl struct{}
48 changes: 25 additions & 23 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const (
)

var thePool = sync.Pool{New: func() any {
return new(syscall15Args)
return new(syscallArgs)
}}

// RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name).
Expand Down Expand Up @@ -141,6 +141,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
// to avoid crashing with too many arguments
var ints int
var floats int
floatArgRegs := numOfFloatRegisters()
var stack int
for i := 0; i < ty.NumIn(); i++ {
arg := ty.In(i)
Expand All @@ -167,7 +168,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
stack++
}
case reflect.Float32, reflect.Float64:
if floats < numOfFloatRegisters() {
if floats < floatArgRegs {
floats++
} else {
stack++
Expand Down Expand Up @@ -202,10 +203,15 @@ func RegisterFunc(fptr any, cfn uintptr) {
}
}

sizeOfStack := maxArgs - numOfIntegerRegisters()
// On Darwin ARM64, use byte-based validation since arguments pack efficiently.
// See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
argsLimit := maxArgs
sizeOfStack := argsLimit - numOfIntegerRegisters()
if runtime.GOOS == "windows" {
if ints+floats+stack > argsLimit {
panic("purego: too many stack arguments")
}
} else if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
// On Darwin ARM64, use byte-based validation since arguments pack efficiently.
// See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
stackBytes := estimateStackBytes(ty)
maxStackBytes := sizeOfStack * 8
if stackBytes > maxStackBytes {
Expand All @@ -224,6 +230,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
// since numOfFloatRegisters() is a function call, not a constant.
// maxArgs is always greater than or equal to numOfFloatRegisters() so this is safe.
var floats [maxArgs]uintptr
floatArgRegs := numOfFloatRegisters()
var numInts int
var numFloats int
var numStack int
Expand All @@ -243,7 +250,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
}
}
addFloat = func(x uintptr) {
if numFloats < numOfFloatRegisters() {
if numFloats < floatArgRegs {
floats[numFloats] = x
numFloats++
} else {
Comment thread
tmc marked this conversation as resolved.
Expand All @@ -257,6 +264,9 @@ func RegisterFunc(fptr any, cfn uintptr) {
// This is in contrast to how macOS and Linux pass arguments which
// tries to use as many registers as possible in the calling convention.
addStack = func(x uintptr) {
if numStack >= maxArgs {
panic("purego: too many stack arguments")
}
sysargs[numStack] = x
numStack++
}
Expand Down Expand Up @@ -310,24 +320,16 @@ func RegisterFunc(fptr any, cfn uintptr) {
keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
}

syscall := thePool.Get().(*syscall15Args)
defer thePool.Put(syscall)

if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
syscall.Set(cfn, sysargs[:], floats[:], 0)
runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
} else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
// Use the normal arm64 calling convention even on Windows
syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
} else {
*syscall = syscall15Args{}
// This is a fallback for Windows amd64, 386, and arm. Note this may not support floats
syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4],
sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
sysargs[12], sysargs[13], sysargs[14])
var syscall *syscallArgs
if runtime.GOOS == "windows" && runtime.GOARCH != "arm64" {
// Windows amd64, 386, and arm use syscall.SyscallN.
syscall = thePool.Get().(*syscallArgs)
syscall.a1, syscall.a2, _ = syscall_syscallN(cfn, sysargs[:numStack]...)
syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
} else {
syscall = syscall_SyscallN(cfn, sysargs[:], floats[:], arm64_r8)
}
defer thePool.Put(syscall)
if ty.NumOut() == 0 {
return nil
}
Expand Down
187 changes: 161 additions & 26 deletions func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,8 @@ func TestABI_ArgumentPassing(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
if tt.name == "20_int32" && runtime.GOARCH == "ppc64le" {
t.Skip("ppc64le retains the 15-argument limit")
}
if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
t.Skip("float32 stack arguments not yet supported on this platform")
Expand All @@ -394,39 +394,174 @@ func TestABI_ArgumentPassing(t *testing.T) {
}
})
}
}

func TestABI_TooManyArguments(t *testing.T) {
if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
t.Skip("This test is specific to Darwin ARM64")
}
t.Run("20_uintptr", func(t *testing.T) {
if runtime.GOARCH == "ppc64le" {
t.Skip("ppc64le retains the 15-argument limit")
}
var fn func(uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr) uintptr
purego.RegisterLibFunc(&fn, lib, "stack_20_uintptr")
got := fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
const want = uintptr(210)
if got != want {
t.Fatalf("stack_20_uintptr: got %d, want %d", got, want)
}
})

libFileName := filepath.Join(t.TempDir(), "abitest.so")
if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
t.Fatal(err)
}
lib, err := load.OpenLibrary(libFileName)
if err != nil {
t.Fatalf("Failed to open library %q: %v", libFileName, err)
}
t.Cleanup(func() {
if err := load.CloseLibrary(lib); err != nil {
t.Errorf("Failed to close library: %v", err)
t.Run("32_uintptr", func(t *testing.T) {
if runtime.GOARCH == "ppc64le" {
Comment thread
TotallyGamerJet marked this conversation as resolved.
t.Skip("ppc64le retains the 15-argument limit")
}
var fn func(
uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
) uintptr
purego.RegisterLibFunc(&fn, lib, "stack_32_uintptr")
got := fn(
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32,
)
const want = uintptr(528)
if got != want {
t.Fatalf("stack_32_uintptr: got %d, want %d", got, want)
}
})

t.Run("syscalln_20_uintptr", func(t *testing.T) {
if runtime.GOARCH == "ppc64le" {
t.Skip("ppc64le retains the 15-argument limit")
}
fn, err := load.OpenSymbol(lib, "stack_20_uintptr")
if err != nil {
t.Fatalf("OpenSymbol(stack_20_uintptr) failed: %v", err)
}
got, _, _ := purego.SyscallN(fn,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
)
const want = uintptr(210)
if got != want {
t.Fatalf("stack_20_uintptr SyscallN: got %d, want %d", got, want)
}
})

t.Run("syscalln_32_uintptr", func(t *testing.T) {
if runtime.GOARCH == "ppc64le" {
t.Skip("ppc64le retains the 15-argument limit")
}
fn, err := load.OpenSymbol(lib, "stack_32_uintptr")
if err != nil {
t.Fatalf("OpenSymbol(stack_32_uintptr) failed: %v", err)
}
got, _, _ := purego.SyscallN(fn,
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32,
)
const want = uintptr(528)
if got != want {
t.Fatalf("stack_32_uintptr SyscallN: got %d, want %d", got, want)
}
})

t.Run("32_mixed_int_float", func(t *testing.T) {
if unsafe.Sizeof(uintptr(0)) == 4 {
t.Skip("requires 64-bit uintptr slots")
}
if runtime.GOARCH == "ppc64le" {
t.Skip("mixed int/float stack arguments are not yet supported on ppc64le")
}

var fn func(
uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
float64, float64, float64, float64, float64, float64, float64, float64,
float64, float64, float64, float64, float64, float64, float64, float64,
) float64
purego.RegisterLibFunc(&fn, lib, "stack_32_mixed_int_float")
got := fn(
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16,
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16,
)
const want = 5168.0
if got != want {
t.Fatalf("stack_32_mixed_int_float: got %f, want %f", got, want)
}
})
}

// Test that 35 int64 arguments (27 slots needed) exceeds the limit
t.Run("35_int64_exceeds_limit", func(t *testing.T) {
func TestABI_TooManyArguments(t *testing.T) {
mustPanic := func(t *testing.T, want string, f func()) {
t.Helper()
defer func() {
if r := recover(); r != nil {
t.Logf("Got expected panic: %v", r)
} else {
t.Errorf("Expected panic but didn't get one")
r := recover()
if r == nil {
t.Fatalf("expected panic %q, got none", want)
}
got := fmt.Sprint(r)
if got != want {
t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, want)
}
}()
f()
}

// 33 int64 parameters exceeds maxArgs=32.
t.Run("registerfunc_33_int64_exceeds_limit", func(t *testing.T) {
mustPanic(t, "purego: too many stack arguments", func() {
var fn func(
int64, int64, int64, int64, int64, int64, int64, int64,
int64, int64, int64, int64, int64, int64, int64, int64,
int64, int64, int64, int64, int64, int64, int64, int64,
int64, int64, int64, int64, int64, int64, int64, int64,
int64,
)
purego.RegisterFunc(&fn, 1)
})
})

var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64)
purego.RegisterLibFunc(&fn, lib, "stack_35_int64_exceeds")
t.Run("registerfunc_16_int64_exceeds_ppc64le_limit", func(t *testing.T) {
if runtime.GOARCH != "ppc64le" {
t.Skip("ppc64le retains the 15-argument limit")
}
mustPanic(t, "purego: too many stack arguments", func() {
var fn func(
int64, int64, int64, int64, int64, int64, int64, int64,
int64, int64, int64, int64, int64, int64, int64, int64,
)
purego.RegisterFunc(&fn, 1)
})
})

t.Run("syscalln_33_uintptr_exceeds_limit", func(t *testing.T) {
mustPanic(t, "purego: too many arguments to SyscallN", func() {
purego.SyscallN(1,
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32,
33,
)
})
})

t.Run("syscalln_16_uintptr_exceeds_ppc64le_limit", func(t *testing.T) {
if runtime.GOARCH != "ppc64le" {
t.Skip("ppc64le retains the 15-argument limit")
}
mustPanic(t, "purego: too many arguments to SyscallN", func() {
purego.SyscallN(1,
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16,
)
})
})
}

Expand Down
38 changes: 16 additions & 22 deletions internal/cgo/syscall_cgo_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,34 @@ package cgo
#include <errno.h>
#include <assert.h>

typedef struct syscall15Args {
typedef struct syscallArgs {
uintptr_t fn;
uintptr_t a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15;
Comment thread
tmc marked this conversation as resolved.
uintptr_t a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32;
uintptr_t f1, f2, f3, f4, f5, f6, f7, f8;
uintptr_t err;
} syscall15Args;
uintptr_t arm64_r8;
} syscallArgs;

void syscall15(struct syscall15Args *args) {
void syscall15(struct syscallArgs *args) {
assert((args->f1|args->f2|args->f3|args->f4|args->f5|args->f6|args->f7|args->f8) == 0);
uintptr_t (*func_name)(uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, uintptr_t a6,
uintptr_t a7, uintptr_t a8, uintptr_t a9, uintptr_t a10, uintptr_t a11, uintptr_t a12,
uintptr_t a13, uintptr_t a14, uintptr_t a15);
uintptr_t a13, uintptr_t a14, uintptr_t a15, uintptr_t a16, uintptr_t a17, uintptr_t a18,
uintptr_t a19, uintptr_t a20, uintptr_t a21, uintptr_t a22, uintptr_t a23, uintptr_t a24,
uintptr_t a25, uintptr_t a26, uintptr_t a27, uintptr_t a28, uintptr_t a29, uintptr_t a30,
uintptr_t a31, uintptr_t a32);
*(void**)(&func_name) = (void*)(args->fn);
uintptr_t r1 = func_name(args->a1,args->a2,args->a3,args->a4,args->a5,args->a6,args->a7,args->a8,args->a9,
args->a10,args->a11,args->a12,args->a13,args->a14,args->a15);
uintptr_t r1 = func_name(args->a1,args->a2,args->a3,args->a4,args->a5,args->a6,args->a7,args->a8,args->a9,
args->a10,args->a11,args->a12,args->a13,args->a14,args->a15,args->a16,args->a17,args->a18,
args->a19,args->a20,args->a21,args->a22,args->a23,args->a24,args->a25,args->a26,args->a27,
args->a28,args->a29,args->a30,args->a31,args->a32);
args->a1 = r1;
args->err = errno;
args->a3 = errno;
}

*/
import "C"
import "unsafe"

// assign purego.syscall15XABI0 to the C version of this function.
var Syscall15XABI0 = unsafe.Pointer(C.syscall15)

//go:nosplit
func Syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) {
args := C.syscall15Args{
C.uintptr_t(fn), C.uintptr_t(a1), C.uintptr_t(a2), C.uintptr_t(a3),
C.uintptr_t(a4), C.uintptr_t(a5), C.uintptr_t(a6),
C.uintptr_t(a7), C.uintptr_t(a8), C.uintptr_t(a9), C.uintptr_t(a10), C.uintptr_t(a11), C.uintptr_t(a12),
C.uintptr_t(a13), C.uintptr_t(a14), C.uintptr_t(a15), 0, 0, 0, 0, 0, 0, 0, 0, 0,
}
C.syscall15(&args)
return uintptr(args.a1), 0, uintptr(args.err)
}
// assign purego.syscallXABI0 to the C version of this function.
var SyscallXABI0 = unsafe.Pointer(C.syscall15)
2 changes: 1 addition & 1 deletion struct_386.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
panic("purego: struct arguments are not supported")
}

func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) {
panic("purego: struct returns are not supported")
}

Expand Down
Loading