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
119 changes: 94 additions & 25 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ import (
"math"
"reflect"
"runtime"
"strconv"
"sync"
"unsafe"

"github.com/ebitengine/purego/internal/strings"
"github.com/ebitengine/purego/internal/xreflect"
)

const (
align8ByteMask = 7 // Mask for 8-byte alignment: (val + 7) &^ 7
align8ByteSize = 8 // 8-byte alignment boundary
)

var thePool = sync.Pool{New: func() any {
return new(syscall15Args)
}}
Expand Down Expand Up @@ -201,11 +205,27 @@ func RegisterFunc(fptr any, cfn uintptr) {
ints++
}
}

sizeOfStack := maxArgs - numOfIntegerRegisters()
if stack > sizeOfStack {
panic("purego: too many arguments")
// On Darwin ARM64, use byte-based validation since arguments pack efficiently
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
stackBytes := estimateStackBytes(ty)
maxStackBytes := sizeOfStack * 8
if stackBytes > maxStackBytes {
panic("purego: too many stack arguments")
}
} else {
if stack > sizeOfStack {
panic("purego: too many stack arguments")
}
}
}

// Detect if cfn is a callback (to avoid tight packing for callbacks which still use 8-byte slots)
// TODO: Remove this check once Darwin ARM64 callback unpacking is updated to handle C-style tight packing.
// When callbacks can unpack tightly-packed arguments, this workaround can be removed.
isCallback := isCallbackFunction(cfn)

v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
var sysargs [maxArgs]uintptr
var floats [numOfFloatRegisters]uintptr
Expand Down Expand Up @@ -281,28 +301,17 @@ func RegisterFunc(fptr any, cfn uintptr) {
}
continue
}
if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" &&
(numInts >= numOfIntegerRegisters() || numFloats >= numOfFloatRegisters) && v.Kind() != reflect.Struct { // hit the stack
fields := make([]reflect.StructField, len(args[i:]))
// Check if we need to start Darwin ARM64 C-style stack packing
// Skip tight packing for callbacks since they still use 8-byte slot unpacking
// TODO: Remove !isCallback condition once callback unpacking supports tight packing
if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && !isCallback && shouldBundleStackArgs(v, numInts, numFloats) {
// Collect and separate remaining args into register vs stack
stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
keepAlive = newKeepAlive

for j, val := range args[i:] {
if val.Kind() == reflect.String {
ptr := strings.CString(val.String())
keepAlive = append(keepAlive, ptr)
val = reflect.ValueOf(ptr)
args[i+j] = val
}
fields[j] = reflect.StructField{
Name: "X" + strconv.Itoa(j),
Type: val.Type(),
}
}
structType := reflect.StructOf(fields)
structInstance := reflect.New(structType).Elem()
for j, val := range args[i:] {
structInstance.Field(j).Set(val)
}
placeRegisters(structInstance, addFloat, addInt)
// Bundle stack arguments with C-style packing
bundleStackArgs(stackArgs, addStack)
break
}
keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
Expand Down Expand Up @@ -473,7 +482,7 @@ func checkStructFieldsSupported(ty reflect.Type) {
}

func roundUpTo8(val uintptr) uintptr {
return (val + 7) &^ 7
return (val + align8ByteMask) &^ align8ByteMask
}

func numOfIntegerRegisters() int {
Expand All @@ -488,3 +497,63 @@ func numOfIntegerRegisters() int {
return maxArgs
}
}

// estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.
// This is a conservative estimate used only for early error detection.
func estimateStackBytes(ty reflect.Type) int {
numInts, numFloats := 0, 0
stackBytes := 0

for i := 0; i < ty.NumIn(); i++ {
arg := ty.In(i)
size := int(arg.Size())

// Check if this goes to register or stack
usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64
if usesInt && numInts < numOfIntegerRegisters() {
numInts++
} else if !usesInt && numFloats < numOfFloatRegisters {
numFloats++
} else {
// Goes to stack - accumulate total bytes
stackBytes += size
}
}
// Round total to 8-byte boundary
if stackBytes > 0 && stackBytes%align8ByteSize != 0 {
stackBytes = (stackBytes + align8ByteMask) &^ align8ByteMask
}
return stackBytes
}

// isCallbackFunction checks if the given function pointer is a purego callback.
// We need to detect this to avoid using tight packing for callbacks, since callback
// unpacking still uses the 8-byte slot convention.
// TODO: This function can be removed once Darwin ARM64 callbacks support tight packing.
// Once callbackWrap is updated to unpack C-style arguments, callbacks can use the same
// tight packing as normal C function calls.
func isCallbackFunction(cfn uintptr) bool {
// Only platforms with syscall_sysv.go have callback detection.
// Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd
hasSyscallSysv := runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" ||
(runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64"))
if !hasSyscallSysv {
return false
}

// Determine callback entry size based on architecture
var entrySize int
switch runtime.GOARCH {
case "386", "amd64":
entrySize = 5
case "arm", "arm64", "loong64":
entrySize = 8
default:
return false
}

// Check if cfn is in the callback address range
callbackStart := getCallbackStart()
callbackEnd := callbackStart + uintptr(getMaxCB()*entrySize)
return cfn >= callbackStart && cfn < callbackEnd
}
231 changes: 231 additions & 0 deletions func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,237 @@ func TestABI(t *testing.T) {
}
}

func TestABI_ArgumentPassing(t *testing.T) {
if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
}
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("Dlopen(%q) failed: %v", libFileName, err)
}
t.Cleanup(func() {
if err := load.CloseLibrary(lib); err != nil {
t.Errorf("Failed to close library: %v", err)
}
})

tests := []struct {
name string
fn interface{}
cFn string
call func(interface{}) string
want string
}{
{
name: "10_int32_baseline",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
cFn: "stack_10_int32",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:9:10",
},
{
name: "11_int32",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
cFn: "stack_11_int32",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:9:10:11",
},
{
name: "10_float32",
fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)),
cFn: "stack_10_float32",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)))(&buf[0], 256, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0)
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
},
{
name: "mixed_stack_strings",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)),
cFn: "stack_mixed_stack_4args",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, "foo", false, 99, "bar")
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:foo:0:99:bar",
},
{
name: "20_int32",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
cFn: "stack_20_int32",
call: func(f interface{}) string {
buf := make([]byte, 512)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 512, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20",
},
{
name: "8int_hfa2_stack",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
cFn: "stack_8int_hfa2_stack",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y float32 }{10.0, 20.0})
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:10.0:20.0",
},
{
name: "8int_2structs_stack",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })),
cFn: "stack_8int_2structs_stack",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y int32 }{9, 10}, struct{ x, y int32 }{11, 12})
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:9:10:11:12",
},
{
name: "8float_hfa2_stack",
fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })),
cFn: "stack_8float_hfa2_stack",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })))(&buf[0], 256, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, struct{ x, y float32 }{9.0, 10.0})
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
},
{
name: "8int_hfa2_floatregs",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
cFn: "stack_8int_hfa2_floatregs",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y float32 }{10.0, 20.0})
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:10.0:20.0",
},
{
name: "8int_int_struct_int",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)),
cFn: "stack_8int_int_struct_int",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, struct{ x, y int32 }{10, 11}, 12)
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:9:10:11:12",
},
{
name: "8int_hfa4_stack",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })),
cFn: "stack_8int_hfa4_stack",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y, z, w float32 }{10.0, 20.0, 30.0, 40.0})
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0",
},
{
name: "8int_mixed_struct",
fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
a int32
b float32
})),
cFn: "stack_8int_mixed_struct",
call: func(f interface{}) string {
buf := make([]byte, 256)
(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
a int32
b float32
})))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
a int32
b float32
}{9, 10.0})
return string(buf[:strings.IndexByte(string(buf), 0)])
},
want: "1:2:3:4:5:6:7:8:9:10.0",
},
}

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 == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64") {
t.Skip("float32 stack arguments not yet supported on this platform")
}
// Struct tests require Darwin ARM64 or AMD64
if strings.HasPrefix(tt.name, "8int_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
}
if strings.HasPrefix(tt.name, "8float_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
}

purego.RegisterLibFunc(tt.fn, lib, tt.cFn)
got := tt.call(tt.fn)
if got != tt.want {
t.Errorf("%s\n got: %q\n want: %q", tt.cFn, got, tt.want)
}
})
}
}

func TestABI_TooManyArguments(t *testing.T) {
if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
t.Skip("This test is specific to Darwin ARM64")
}

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("Dlopen(%q) failed: %v", libFileName, err)
}
t.Cleanup(func() {
if err := load.CloseLibrary(lib); err != nil {
t.Errorf("Failed to close library: %v", err)
}
})

// Test that 25 int64 arguments (17 slots needed) exceeds the limit
t.Run("25_int64_exceeds_limit", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
msg := fmt.Sprint(r)
if !strings.Contains(msg, "too many stack arguments") {
t.Errorf("Expected detailed error message, got: %v", r)
}
t.Logf("Got expected panic with message: %v", r)
} else {
t.Errorf("Expected panic but didn't get one")
}
}()

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)
purego.RegisterLibFunc(&fn, lib, "stack_25_int64_exceeds")
})
}

func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
out, err := exec.Command("go", "env", compilerEnv).Output()
if err != nil {
Expand Down
Loading