detached 6b66588

purego: fix Darwin ARM64 calling convention for struct arguments, callbacks, and stack packing (#353)

Darwin ARM64 uses byte-aligned argument packing, not 8-byte slots.
The current code uses 8-byte slots for everything, causing two problems:

1. Functions with >8 parameters fail when using int32, float32, etc.
2. Struct arguments are packed incorrectly.

For example:

```
// C: int32_t sum(int32_t a1, ..., int32_t a11)
var fn func(int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32) int32
purego.RegisterLibFunc(&fn, lib, "sum_11_int32")
result := fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
// fails: stack arguments misaligned
```

The fix implements proper Darwin ARM64 calling conventions:

* Stack arguments use natural alignment (int32 = 4 bytes, not 8).
* Multiple small values pack into single 8-byte slots.
* Struct arguments use byte-level copying, not slot alignment.
* Callbacks unpack arguments using Darwin-specific rules.
* Register allocation tries registers before falling back to stack.

The code splits ARM64 struct handling into darwin and non-darwin versions
to preserve correct behavior on Linux and other ARM64 systems.

Closes #352

---------

Co-authored-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
commit
6b665882fe54db38661e06d0dc959ea695db6030
author
Travis Cline · · 2025-12-31 18:16 UTC
committer
GitHub · · 2025-12-31 18:16 UTC
parent
f6151bf
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
12 files · +866 -49
M func.go +97 -25
@@ -10,7 +10,6 @@ import (
1010 "math"
1111 "reflect"
1212 "runtime"
13- "strconv"
1413 "sync"
1514 "unsafe"
1615
@@ -18,6 +17,11 @@ import (
1817 "github.com/ebitengine/purego/internal/xreflect"
1918 )
2019
20+const (
21+ align8ByteMask = 7 // Mask for 8-byte alignment: (val + 7) &^ 7
22+ align8ByteSize = 8 // 8-byte alignment boundary
23+)
24+
2125 var thePool = sync.Pool{New: func() any {
2226 return new(syscall15Args)
2327 }}
@@ -95,6 +99,9 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
9599 // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
96100 // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
97101 //
102+// On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack,
103+// following the C ABI's byte-level packing rules.
104+//
98105 // # Example
99106 //
100107 // All functions below call this C function:
@@ -201,11 +208,27 @@ func RegisterFunc(fptr any, cfn uintptr) {
201208 ints++
202209 }
203210 }
211+
204212 sizeOfStack := maxArgs - numOfIntegerRegisters()
205- if stack > sizeOfStack {
206- panic("purego: too many arguments")
213+ // On Darwin ARM64, use byte-based validation since arguments pack efficiently
214+ if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
215+ stackBytes := estimateStackBytes(ty)
216+ maxStackBytes := sizeOfStack * 8
217+ if stackBytes > maxStackBytes {
218+ panic("purego: too many stack arguments")
219+ }
220+ } else {
221+ if stack > sizeOfStack {
222+ panic("purego: too many stack arguments")
223+ }
207224 }
208225 }
226+
227+ // Detect if cfn is a callback (to avoid tight packing for callbacks which still use 8-byte slots)
228+ // TODO: Remove this check once Darwin ARM64 callback unpacking is updated to handle C-style tight packing.
229+ // When callbacks can unpack tightly-packed arguments, this workaround can be removed.
230+ isCallback := isCallbackFunction(cfn)
231+
209232 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
210233 var sysargs [maxArgs]uintptr
211234 var floats [numOfFloatRegisters]uintptr
@@ -281,28 +304,17 @@ func RegisterFunc(fptr any, cfn uintptr) {
281304 }
282305 continue
283306 }
284- if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" &&
285- (numInts >= numOfIntegerRegisters() || numFloats >= numOfFloatRegisters) && v.Kind() != reflect.Struct { // hit the stack
286- fields := make([]reflect.StructField, len(args[i:]))
307+ // Check if we need to start Darwin ARM64 C-style stack packing
308+ // Skip tight packing for callbacks since they still use 8-byte slot unpacking
309+ // TODO: Remove !isCallback condition once callback unpacking supports tight packing
310+ if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && !isCallback && shouldBundleStackArgs(v, numInts, numFloats) {
311+ // Collect and separate remaining args into register vs stack
312+ stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
313+ keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
314+ keepAlive = newKeepAlive
287315
288- for j, val := range args[i:] {
289- if val.Kind() == reflect.String {
290- ptr := strings.CString(val.String())
291- keepAlive = append(keepAlive, ptr)
292- val = reflect.ValueOf(ptr)
293- args[i+j] = val
294- }
295- fields[j] = reflect.StructField{
296- Name: "X" + strconv.Itoa(j),
297- Type: val.Type(),
298- }
299- }
300- structType := reflect.StructOf(fields)
301- structInstance := reflect.New(structType).Elem()
302- for j, val := range args[i:] {
303- structInstance.Field(j).Set(val)
304- }
305- placeRegisters(structInstance, addFloat, addInt)
316+ // Bundle stack arguments with C-style packing
317+ bundleStackArgs(stackArgs, addStack)
306318 break
307319 }
308320 keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
@@ -473,7 +485,7 @@ func checkStructFieldsSupported(ty reflect.Type) {
473485 }
474486
475487 func roundUpTo8(val uintptr) uintptr {
476- return (val + 7) &^ 7
488+ return (val + align8ByteMask) &^ align8ByteMask
477489 }
478490
479491 func numOfIntegerRegisters() int {
@@ -488,3 +500,63 @@ func numOfIntegerRegisters() int {
488500 return maxArgs
489501 }
490502 }
503+
504+// estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.
505+// This is a conservative estimate used only for early error detection.
506+func estimateStackBytes(ty reflect.Type) int {
507+ numInts, numFloats := 0, 0
508+ stackBytes := 0
509+
510+ for i := 0; i < ty.NumIn(); i++ {
511+ arg := ty.In(i)
512+ size := int(arg.Size())
513+
514+ // Check if this goes to register or stack
515+ usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64
516+ if usesInt && numInts < numOfIntegerRegisters() {
517+ numInts++
518+ } else if !usesInt && numFloats < numOfFloatRegisters {
519+ numFloats++
520+ } else {
521+ // Goes to stack - accumulate total bytes
522+ stackBytes += size
523+ }
524+ }
525+ // Round total to 8-byte boundary
526+ if stackBytes > 0 && stackBytes%align8ByteSize != 0 {
527+ stackBytes = int(roundUpTo8(uintptr(stackBytes)))
528+ }
529+ return stackBytes
530+}
531+
532+// isCallbackFunction checks if the given function pointer is a purego callback.
533+// We need to detect this to avoid using tight packing for callbacks, since callback
534+// unpacking still uses the 8-byte slot convention.
535+// TODO: This function can be removed once Darwin ARM64 callbacks support tight packing.
536+// Once callbackWrap is updated to unpack C-style arguments, callbacks can use the same
537+// tight packing as normal C function calls.
538+func isCallbackFunction(cfn uintptr) bool {
539+ // Only platforms with syscall_sysv.go have callback detection.
540+ // Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd
541+ hasSyscallSysv := runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" ||
542+ (runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64"))
543+ if !hasSyscallSysv {
544+ return false
545+ }
546+
547+ // Determine callback entry size based on architecture
548+ var entrySize int
549+ switch runtime.GOARCH {
550+ case "386", "amd64":
551+ entrySize = 5
552+ case "arm", "arm64", "loong64":
553+ entrySize = 8
554+ default:
555+ return false
556+ }
557+
558+ // Check if cfn is in the callback address range
559+ callbackStart := getCallbackStart()
560+ callbackEnd := callbackStart + uintptr(getMaxCB()*entrySize)
561+ return cfn >= callbackStart && cfn < callbackEnd
562+}
@@ -10,7 +10,6 @@ import (
10 "math"10 "math"
11 "reflect"11 "reflect"
12 "runtime"12 "runtime"
13- "strconv"
14 "sync"13 "sync"
15 "unsafe"14 "unsafe"
16 15
@@ -18,6 +17,11 @@ import (
18 "github.com/ebitengine/purego/internal/xreflect"17 "github.com/ebitengine/purego/internal/xreflect"
19 )18 )
20 19
20+const (
21+ align8ByteMask = 7 // Mask for 8-byte alignment: (val + 7) &^ 7
22+ align8ByteSize = 8 // 8-byte alignment boundary
23+)
24+
21 var thePool = sync.Pool{New: func() any {25 var thePool = sync.Pool{New: func() any {
22 return new(syscall15Args)26 return new(syscall15Args)
23 }}27 }}
@@ -95,6 +99,9 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
95 // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure99 // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
96 // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.100 // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
97 //101 //
102+// On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack,
103+// following the C ABI's byte-level packing rules.
104+//
98 // # Example105 // # Example
99 //106 //
100 // All functions below call this C function:107 // All functions below call this C function:
@@ -201,11 +208,27 @@ func RegisterFunc(fptr any, cfn uintptr) {
201 ints++208 ints++
202 }209 }
203 }210 }
211+
204 sizeOfStack := maxArgs - numOfIntegerRegisters()212 sizeOfStack := maxArgs - numOfIntegerRegisters()
205- if stack > sizeOfStack {213+ // On Darwin ARM64, use byte-based validation since arguments pack efficiently
206- panic("purego: too many arguments")214+ if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
215+ stackBytes := estimateStackBytes(ty)
216+ maxStackBytes := sizeOfStack * 8
217+ if stackBytes > maxStackBytes {
218+ panic("purego: too many stack arguments")
219+ }
220+ } else {
221+ if stack > sizeOfStack {
222+ panic("purego: too many stack arguments")
223+ }
207 }224 }
208 }225 }
226+
227+ // Detect if cfn is a callback (to avoid tight packing for callbacks which still use 8-byte slots)
228+ // TODO: Remove this check once Darwin ARM64 callback unpacking is updated to handle C-style tight packing.
229+ // When callbacks can unpack tightly-packed arguments, this workaround can be removed.
230+ isCallback := isCallbackFunction(cfn)
231+
209 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {232 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
210 var sysargs [maxArgs]uintptr233 var sysargs [maxArgs]uintptr
211 var floats [numOfFloatRegisters]uintptr234 var floats [numOfFloatRegisters]uintptr
@@ -281,28 +304,17 @@ func RegisterFunc(fptr any, cfn uintptr) {
281 }304 }
282 continue305 continue
283 }306 }
284- if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" &&307+ // Check if we need to start Darwin ARM64 C-style stack packing
285- (numInts >= numOfIntegerRegisters() || numFloats >= numOfFloatRegisters) && v.Kind() != reflect.Struct { // hit the stack308+ // Skip tight packing for callbacks since they still use 8-byte slot unpacking
286- fields := make([]reflect.StructField, len(args[i:]))309+ // TODO: Remove !isCallback condition once callback unpacking supports tight packing
310+ if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && !isCallback && shouldBundleStackArgs(v, numInts, numFloats) {
311+ // Collect and separate remaining args into register vs stack
312+ stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
313+ keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
314+ keepAlive = newKeepAlive
287 315
288- for j, val := range args[i:] {316+ // Bundle stack arguments with C-style packing
289- if val.Kind() == reflect.String {317+ bundleStackArgs(stackArgs, addStack)
290- ptr := strings.CString(val.String())
291- keepAlive = append(keepAlive, ptr)
292- val = reflect.ValueOf(ptr)
293- args[i+j] = val
294- }
295- fields[j] = reflect.StructField{
296- Name: "X" + strconv.Itoa(j),
297- Type: val.Type(),
298- }
299- }
300- structType := reflect.StructOf(fields)
301- structInstance := reflect.New(structType).Elem()
302- for j, val := range args[i:] {
303- structInstance.Field(j).Set(val)
304- }
305- placeRegisters(structInstance, addFloat, addInt)
306 break318 break
307 }319 }
308 keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)320 keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
@@ -473,7 +485,7 @@ func checkStructFieldsSupported(ty reflect.Type) {
473 }485 }
474 486
475 func roundUpTo8(val uintptr) uintptr {487 func roundUpTo8(val uintptr) uintptr {
476- return (val + 7) &^ 7488+ return (val + align8ByteMask) &^ align8ByteMask
477 }489 }
478 490
479 func numOfIntegerRegisters() int {491 func numOfIntegerRegisters() int {
@@ -488,3 +500,63 @@ func numOfIntegerRegisters() int {
488 return maxArgs500 return maxArgs
489 }501 }
490 }502 }
503+
504+// estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.
505+// This is a conservative estimate used only for early error detection.
506+func estimateStackBytes(ty reflect.Type) int {
507+ numInts, numFloats := 0, 0
508+ stackBytes := 0
509+
510+ for i := 0; i < ty.NumIn(); i++ {
511+ arg := ty.In(i)
512+ size := int(arg.Size())
513+
514+ // Check if this goes to register or stack
515+ usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64
516+ if usesInt && numInts < numOfIntegerRegisters() {
517+ numInts++
518+ } else if !usesInt && numFloats < numOfFloatRegisters {
519+ numFloats++
520+ } else {
521+ // Goes to stack - accumulate total bytes
522+ stackBytes += size
523+ }
524+ }
525+ // Round total to 8-byte boundary
526+ if stackBytes > 0 && stackBytes%align8ByteSize != 0 {
527+ stackBytes = int(roundUpTo8(uintptr(stackBytes)))
528+ }
529+ return stackBytes
530+}
531+
532+// isCallbackFunction checks if the given function pointer is a purego callback.
533+// We need to detect this to avoid using tight packing for callbacks, since callback
534+// unpacking still uses the 8-byte slot convention.
535+// TODO: This function can be removed once Darwin ARM64 callbacks support tight packing.
536+// Once callbackWrap is updated to unpack C-style arguments, callbacks can use the same
537+// tight packing as normal C function calls.
538+func isCallbackFunction(cfn uintptr) bool {
539+ // Only platforms with syscall_sysv.go have callback detection.
540+ // Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd
541+ hasSyscallSysv := runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" ||
542+ (runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64"))
543+ if !hasSyscallSysv {
544+ return false
545+ }
546+
547+ // Determine callback entry size based on architecture
548+ var entrySize int
549+ switch runtime.GOARCH {
550+ case "386", "amd64":
551+ entrySize = 5
552+ case "arm", "arm64", "loong64":
553+ entrySize = 8
554+ default:
555+ return false
556+ }
557+
558+ // Check if cfn is in the callback address range
559+ callbackStart := getCallbackStart()
560+ callbackEnd := callbackStart + uintptr(getMaxCB()*entrySize)
561+ return cfn >= callbackStart && cfn < callbackEnd
562+}
M func_test.go +229 -1
@@ -4,6 +4,7 @@
44 package purego_test
55
66 import (
7+ "bytes"
78 "errors"
89 "fmt"
910 "os/exec"
@@ -191,7 +192,7 @@ func TestABI(t *testing.T) {
191192 purego.RegisterLibFunc(&fn, lib, cName)
192193 buf := make([]byte, 256)
193194 fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz")
194- res := string(buf[:strings.IndexByte(string(buf), 0)])
195+ res := string(buf[:bytes.IndexByte(buf, 0)])
195196 const want = "1:2:3:4:5:6:7:8:foo:bar:baz"
196197 if res != want {
197198 t.Fatalf("%s: got %q, want %q", cName, res, want)
@@ -199,6 +200,233 @@ func TestABI(t *testing.T) {
199200 }
200201 }
201202
203+func TestABI_ArgumentPassing(t *testing.T) {
204+ if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
205+ t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
206+ }
207+ libFileName := filepath.Join(t.TempDir(), "abitest.so")
208+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
209+ t.Fatal(err)
210+ }
211+ lib, err := load.OpenLibrary(libFileName)
212+ if err != nil {
213+ t.Fatalf("Failed to open library %q: %v", libFileName, err)
214+ }
215+ t.Cleanup(func() {
216+ if err := load.CloseLibrary(lib); err != nil {
217+ t.Errorf("Failed to close library: %v", err)
218+ }
219+ })
220+
221+ tests := []struct {
222+ name string
223+ fn any
224+ cFn string
225+ call func(any) string
226+ want string
227+ }{
228+ {
229+ name: "10_int32_baseline",
230+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
231+ cFn: "stack_10_int32",
232+ call: func(f any) string {
233+ buf := make([]byte, 256)
234+ (*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)
235+ return string(buf[:bytes.IndexByte(buf, 0)])
236+ },
237+ want: "1:2:3:4:5:6:7:8:9:10",
238+ },
239+ {
240+ name: "11_int32",
241+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
242+ cFn: "stack_11_int32",
243+ call: func(f any) string {
244+ buf := make([]byte, 256)
245+ (*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)
246+ return string(buf[:bytes.IndexByte(buf, 0)])
247+ },
248+ want: "1:2:3:4:5:6:7:8:9:10:11",
249+ },
250+ {
251+ name: "10_float32",
252+ fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)),
253+ cFn: "stack_10_float32",
254+ call: func(f any) string {
255+ buf := make([]byte, 256)
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)
257+ return string(buf[:bytes.IndexByte(buf, 0)])
258+ },
259+ want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
260+ },
261+ {
262+ name: "mixed_stack_strings",
263+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)),
264+ cFn: "stack_mixed_stack_4args",
265+ call: func(f any) string {
266+ buf := make([]byte, 256)
267+ (*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")
268+ return string(buf[:bytes.IndexByte(buf, 0)])
269+ },
270+ want: "1:2:3:4:5:6:7:8:foo:0:99:bar",
271+ },
272+ {
273+ name: "20_int32",
274+ 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)),
275+ cFn: "stack_20_int32",
276+ call: func(f any) string {
277+ buf := make([]byte, 512)
278+ (*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)
279+ return string(buf[:bytes.IndexByte(buf, 0)])
280+ },
281+ want: "1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20",
282+ },
283+ {
284+ name: "8int_hfa2_stack",
285+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
286+ cFn: "stack_8int_hfa2_stack",
287+ call: func(f any) string {
288+ buf := make([]byte, 256)
289+ (*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})
290+ return string(buf[:bytes.IndexByte(buf, 0)])
291+ },
292+ want: "1:2:3:4:5:6:7:8:10.0:20.0",
293+ },
294+ {
295+ name: "8int_2structs_stack",
296+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })),
297+ cFn: "stack_8int_2structs_stack",
298+ call: func(f any) string {
299+ buf := make([]byte, 256)
300+ (*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})
301+ return string(buf[:bytes.IndexByte(buf, 0)])
302+ },
303+ want: "1:2:3:4:5:6:7:8:9:10:11:12",
304+ },
305+ {
306+ name: "8float_hfa2_stack",
307+ fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })),
308+ cFn: "stack_8float_hfa2_stack",
309+ call: func(f any) string {
310+ buf := make([]byte, 256)
311+ (*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})
312+ return string(buf[:bytes.IndexByte(buf, 0)])
313+ },
314+ want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
315+ },
316+ {
317+ name: "8int_hfa2_floatregs",
318+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
319+ cFn: "stack_8int_hfa2_floatregs",
320+ call: func(f any) string {
321+ buf := make([]byte, 256)
322+ (*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})
323+ return string(buf[:bytes.IndexByte(buf, 0)])
324+ },
325+ want: "1:2:3:4:5:6:7:8:10.0:20.0",
326+ },
327+ {
328+ name: "8int_int_struct_int",
329+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)),
330+ cFn: "stack_8int_int_struct_int",
331+ call: func(f any) string {
332+ buf := make([]byte, 256)
333+ (*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)
334+ return string(buf[:bytes.IndexByte(buf, 0)])
335+ },
336+ want: "1:2:3:4:5:6:7:8:9:10:11:12",
337+ },
338+ {
339+ name: "8int_hfa4_stack",
340+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })),
341+ cFn: "stack_8int_hfa4_stack",
342+ call: func(f any) string {
343+ buf := make([]byte, 256)
344+ (*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})
345+ return string(buf[:bytes.IndexByte(buf, 0)])
346+ },
347+ want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0",
348+ },
349+ {
350+ name: "8int_mixed_struct",
351+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
352+ a int32
353+ b float32
354+ })),
355+ cFn: "stack_8int_mixed_struct",
356+ call: func(f any) string {
357+ buf := make([]byte, 256)
358+ (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
359+ a int32
360+ b float32
361+ })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
362+ a int32
363+ b float32
364+ }{9, 10.0})
365+ return string(buf[:bytes.IndexByte(buf, 0)])
366+ },
367+ want: "1:2:3:4:5:6:7:8:9:10.0",
368+ },
369+ }
370+
371+ for _, tt := range tests {
372+ t.Run(tt.name, func(t *testing.T) {
373+ if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
374+ t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
375+ }
376+ if tt.name == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64") {
377+ t.Skip("float32 stack arguments not yet supported on this platform")
378+ }
379+ // Struct tests require Darwin ARM64 or AMD64
380+ if strings.HasPrefix(tt.name, "8int_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
381+ t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
382+ }
383+ if strings.HasPrefix(tt.name, "8float_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
384+ t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
385+ }
386+
387+ purego.RegisterLibFunc(tt.fn, lib, tt.cFn)
388+ got := tt.call(tt.fn)
389+ if got != tt.want {
390+ t.Errorf("%s\n got: %q\n want: %q", tt.cFn, got, tt.want)
391+ }
392+ })
393+ }
394+}
395+
396+func TestABI_TooManyArguments(t *testing.T) {
397+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
398+ t.Skip("This test is specific to Darwin ARM64")
399+ }
400+
401+ libFileName := filepath.Join(t.TempDir(), "abitest.so")
402+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
403+ t.Fatal(err)
404+ }
405+ lib, err := load.OpenLibrary(libFileName)
406+ if err != nil {
407+ t.Fatalf("Failed to open library %q: %v", libFileName, err)
408+ }
409+ t.Cleanup(func() {
410+ if err := load.CloseLibrary(lib); err != nil {
411+ t.Errorf("Failed to close library: %v", err)
412+ }
413+ })
414+
415+ // Test that 25 int64 arguments (17 slots needed) exceeds the limit
416+ t.Run("25_int64_exceeds_limit", func(t *testing.T) {
417+ defer func() {
418+ if r := recover(); r != nil {
419+ t.Logf("Got expected panic: %v", r)
420+ } else {
421+ t.Errorf("Expected panic but didn't get one")
422+ }
423+ }()
424+
425+ 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)
426+ purego.RegisterLibFunc(&fn, lib, "stack_25_int64_exceeds")
427+ })
428+}
429+
202430 func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
203431 out, err := exec.Command("go", "env", compilerEnv).Output()
204432 if err != nil {
@@ -4,6 +4,7 @@
4 package purego_test4 package purego_test
5 5
6 import (6 import (
7+ "bytes"
7 "errors"8 "errors"
8 "fmt"9 "fmt"
9 "os/exec"10 "os/exec"
@@ -191,7 +192,7 @@ func TestABI(t *testing.T) {
191 purego.RegisterLibFunc(&fn, lib, cName)192 purego.RegisterLibFunc(&fn, lib, cName)
192 buf := make([]byte, 256)193 buf := make([]byte, 256)
193 fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz")194 fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz")
194- res := string(buf[:strings.IndexByte(string(buf), 0)])195+ res := string(buf[:bytes.IndexByte(buf, 0)])
195 const want = "1:2:3:4:5:6:7:8:foo:bar:baz"196 const want = "1:2:3:4:5:6:7:8:foo:bar:baz"
196 if res != want {197 if res != want {
197 t.Fatalf("%s: got %q, want %q", cName, res, want)198 t.Fatalf("%s: got %q, want %q", cName, res, want)
@@ -199,6 +200,233 @@ func TestABI(t *testing.T) {
199 }200 }
200 }201 }
201 202
203+func TestABI_ArgumentPassing(t *testing.T) {
204+ if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
205+ t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
206+ }
207+ libFileName := filepath.Join(t.TempDir(), "abitest.so")
208+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
209+ t.Fatal(err)
210+ }
211+ lib, err := load.OpenLibrary(libFileName)
212+ if err != nil {
213+ t.Fatalf("Failed to open library %q: %v", libFileName, err)
214+ }
215+ t.Cleanup(func() {
216+ if err := load.CloseLibrary(lib); err != nil {
217+ t.Errorf("Failed to close library: %v", err)
218+ }
219+ })
220+
221+ tests := []struct {
222+ name string
223+ fn any
224+ cFn string
225+ call func(any) string
226+ want string
227+ }{
228+ {
229+ name: "10_int32_baseline",
230+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
231+ cFn: "stack_10_int32",
232+ call: func(f any) string {
233+ buf := make([]byte, 256)
234+ (*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)
235+ return string(buf[:bytes.IndexByte(buf, 0)])
236+ },
237+ want: "1:2:3:4:5:6:7:8:9:10",
238+ },
239+ {
240+ name: "11_int32",
241+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
242+ cFn: "stack_11_int32",
243+ call: func(f any) string {
244+ buf := make([]byte, 256)
245+ (*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)
246+ return string(buf[:bytes.IndexByte(buf, 0)])
247+ },
248+ want: "1:2:3:4:5:6:7:8:9:10:11",
249+ },
250+ {
251+ name: "10_float32",
252+ fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)),
253+ cFn: "stack_10_float32",
254+ call: func(f any) string {
255+ buf := make([]byte, 256)
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)
257+ return string(buf[:bytes.IndexByte(buf, 0)])
258+ },
259+ want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
260+ },
261+ {
262+ name: "mixed_stack_strings",
263+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)),
264+ cFn: "stack_mixed_stack_4args",
265+ call: func(f any) string {
266+ buf := make([]byte, 256)
267+ (*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")
268+ return string(buf[:bytes.IndexByte(buf, 0)])
269+ },
270+ want: "1:2:3:4:5:6:7:8:foo:0:99:bar",
271+ },
272+ {
273+ name: "20_int32",
274+ 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)),
275+ cFn: "stack_20_int32",
276+ call: func(f any) string {
277+ buf := make([]byte, 512)
278+ (*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)
279+ return string(buf[:bytes.IndexByte(buf, 0)])
280+ },
281+ want: "1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20",
282+ },
283+ {
284+ name: "8int_hfa2_stack",
285+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
286+ cFn: "stack_8int_hfa2_stack",
287+ call: func(f any) string {
288+ buf := make([]byte, 256)
289+ (*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})
290+ return string(buf[:bytes.IndexByte(buf, 0)])
291+ },
292+ want: "1:2:3:4:5:6:7:8:10.0:20.0",
293+ },
294+ {
295+ name: "8int_2structs_stack",
296+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })),
297+ cFn: "stack_8int_2structs_stack",
298+ call: func(f any) string {
299+ buf := make([]byte, 256)
300+ (*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})
301+ return string(buf[:bytes.IndexByte(buf, 0)])
302+ },
303+ want: "1:2:3:4:5:6:7:8:9:10:11:12",
304+ },
305+ {
306+ name: "8float_hfa2_stack",
307+ fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })),
308+ cFn: "stack_8float_hfa2_stack",
309+ call: func(f any) string {
310+ buf := make([]byte, 256)
311+ (*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})
312+ return string(buf[:bytes.IndexByte(buf, 0)])
313+ },
314+ want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
315+ },
316+ {
317+ name: "8int_hfa2_floatregs",
318+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
319+ cFn: "stack_8int_hfa2_floatregs",
320+ call: func(f any) string {
321+ buf := make([]byte, 256)
322+ (*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})
323+ return string(buf[:bytes.IndexByte(buf, 0)])
324+ },
325+ want: "1:2:3:4:5:6:7:8:10.0:20.0",
326+ },
327+ {
328+ name: "8int_int_struct_int",
329+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)),
330+ cFn: "stack_8int_int_struct_int",
331+ call: func(f any) string {
332+ buf := make([]byte, 256)
333+ (*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)
334+ return string(buf[:bytes.IndexByte(buf, 0)])
335+ },
336+ want: "1:2:3:4:5:6:7:8:9:10:11:12",
337+ },
338+ {
339+ name: "8int_hfa4_stack",
340+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })),
341+ cFn: "stack_8int_hfa4_stack",
342+ call: func(f any) string {
343+ buf := make([]byte, 256)
344+ (*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})
345+ return string(buf[:bytes.IndexByte(buf, 0)])
346+ },
347+ want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0",
348+ },
349+ {
350+ name: "8int_mixed_struct",
351+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
352+ a int32
353+ b float32
354+ })),
355+ cFn: "stack_8int_mixed_struct",
356+ call: func(f any) string {
357+ buf := make([]byte, 256)
358+ (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
359+ a int32
360+ b float32
361+ })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
362+ a int32
363+ b float32
364+ }{9, 10.0})
365+ return string(buf[:bytes.IndexByte(buf, 0)])
366+ },
367+ want: "1:2:3:4:5:6:7:8:9:10.0",
368+ },
369+ }
370+
371+ for _, tt := range tests {
372+ t.Run(tt.name, func(t *testing.T) {
373+ if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
374+ t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
375+ }
376+ if tt.name == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64") {
377+ t.Skip("float32 stack arguments not yet supported on this platform")
378+ }
379+ // Struct tests require Darwin ARM64 or AMD64
380+ if strings.HasPrefix(tt.name, "8int_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
381+ t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
382+ }
383+ if strings.HasPrefix(tt.name, "8float_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
384+ t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
385+ }
386+
387+ purego.RegisterLibFunc(tt.fn, lib, tt.cFn)
388+ got := tt.call(tt.fn)
389+ if got != tt.want {
390+ t.Errorf("%s\n got: %q\n want: %q", tt.cFn, got, tt.want)
391+ }
392+ })
393+ }
394+}
395+
396+func TestABI_TooManyArguments(t *testing.T) {
397+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
398+ t.Skip("This test is specific to Darwin ARM64")
399+ }
400+
401+ libFileName := filepath.Join(t.TempDir(), "abitest.so")
402+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
403+ t.Fatal(err)
404+ }
405+ lib, err := load.OpenLibrary(libFileName)
406+ if err != nil {
407+ t.Fatalf("Failed to open library %q: %v", libFileName, err)
408+ }
409+ t.Cleanup(func() {
410+ if err := load.CloseLibrary(lib); err != nil {
411+ t.Errorf("Failed to close library: %v", err)
412+ }
413+ })
414+
415+ // Test that 25 int64 arguments (17 slots needed) exceeds the limit
416+ t.Run("25_int64_exceeds_limit", func(t *testing.T) {
417+ defer func() {
418+ if r := recover(); r != nil {
419+ t.Logf("Got expected panic: %v", r)
420+ } else {
421+ t.Errorf("Expected panic but didn't get one")
422+ }
423+ }()
424+
425+ 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)
426+ purego.RegisterLibFunc(&fn, lib, "stack_25_int64_exceeds")
427+ })
428+}
429+
202 func buildSharedLib(compilerEnv, libFile string, sources ...string) error {430 func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
203 out, err := exec.Command("go", "env", compilerEnv).Output()431 out, err := exec.Command("go", "env", compilerEnv).Output()
204 if err != nil {432 if err != nil {
A struct_386.go +41 -0
@@ -0,0 +1,41 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package purego
5+
6+import "reflect"
7+
8+func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
9+ panic("purego: struct arguments are not supported")
10+}
11+
12+func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
13+ panic("purego: struct returns are not supported")
14+}
15+
16+func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
17+ panic("purego: placeRegisters not implemented on 386")
18+}
19+
20+// shouldBundleStackArgs always returns false on 386
21+// since C-style stack argument bundling is only needed on Darwin ARM64.
22+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
23+ return false
24+}
25+
26+// structFitsInRegisters is not used on 386.
27+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
28+ panic("purego: structFitsInRegisters should not be called on 386")
29+}
30+
31+// collectStackArgs is not used on 386.
32+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
33+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
34+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
35+ panic("purego: collectStackArgs should not be called on 386")
36+}
37+
38+// bundleStackArgs is not used on 386.
39+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
40+ panic("purego: bundleStackArgs should not be called on 386")
41+}
@@ -0,0 +1,41 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package purego
5+
6+import "reflect"
7+
8+func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
9+ panic("purego: struct arguments are not supported")
10+}
11+
12+func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
13+ panic("purego: struct returns are not supported")
14+}
15+
16+func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
17+ panic("purego: placeRegisters not implemented on 386")
18+}
19+
20+// shouldBundleStackArgs always returns false on 386
21+// since C-style stack argument bundling is only needed on Darwin ARM64.
22+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
23+ return false
24+}
25+
26+// structFitsInRegisters is not used on 386.
27+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
28+ panic("purego: structFitsInRegisters should not be called on 386")
29+}
30+
31+// collectStackArgs is not used on 386.
32+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
33+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
34+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
35+ panic("purego: collectStackArgs should not be called on 386")
36+}
37+
38+// bundleStackArgs is not used on 386.
39+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
40+ panic("purego: bundleStackArgs should not be called on 386")
41+}
M struct_amd64.go +24 -1
@@ -260,5 +260,28 @@ func placeStack(v reflect.Value, addStack func(uintptr)) {
260260 }
261261
262262 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
263- panic("purego: not needed on amd64")
263+ panic("purego: placeRegisters not implemented on amd64")
264+}
265+
266+// shouldBundleStackArgs always returns false on non-Darwin platforms
267+// since C-style stack argument bundling is only needed on Darwin ARM64.
268+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
269+ return false
270+}
271+
272+// structFitsInRegisters is not used on amd64.
273+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
274+ panic("purego: structFitsInRegisters should not be called on amd64")
275+}
276+
277+// collectStackArgs is not used on amd64.
278+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
279+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
280+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
281+ panic("purego: collectStackArgs should not be called on amd64")
282+}
283+
284+// bundleStackArgs is not used on amd64.
285+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
286+ panic("purego: bundleStackArgs should not be called on amd64")
264287 }
@@ -260,5 +260,28 @@ func placeStack(v reflect.Value, addStack func(uintptr)) {
260 }260 }
261 261
262 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {262 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
263- panic("purego: not needed on amd64")263+ panic("purego: placeRegisters not implemented on amd64")
264+}
265+
266+// shouldBundleStackArgs always returns false on non-Darwin platforms
267+// since C-style stack argument bundling is only needed on Darwin ARM64.
268+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
269+ return false
270+}
271+
272+// structFitsInRegisters is not used on amd64.
273+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
274+ panic("purego: structFitsInRegisters should not be called on amd64")
275+}
276+
277+// collectStackArgs is not used on amd64.
278+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
279+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
280+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
281+ panic("purego: collectStackArgs should not be called on amd64")
282+}
283+
284+// bundleStackArgs is not used on amd64.
285+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
286+ panic("purego: bundleStackArgs should not be called on amd64")
264 }287 }
A struct_arm.go +41 -0
@@ -0,0 +1,41 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package purego
5+
6+import "reflect"
7+
8+func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
9+ panic("purego: struct arguments are not supported")
10+}
11+
12+func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
13+ panic("purego: struct returns are not supported")
14+}
15+
16+func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
17+ panic("purego: placeRegisters not implemented on arm")
18+}
19+
20+// shouldBundleStackArgs always returns false on arm
21+// since C-style stack argument bundling is only needed on Darwin ARM64.
22+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
23+ return false
24+}
25+
26+// structFitsInRegisters is not used on arm.
27+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
28+ panic("purego: structFitsInRegisters should not be called on arm")
29+}
30+
31+// collectStackArgs is not used on arm.
32+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
33+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
34+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
35+ panic("purego: collectStackArgs should not be called on arm")
36+}
37+
38+// bundleStackArgs is not used on arm.
39+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
40+ panic("purego: bundleStackArgs should not be called on arm")
41+}
@@ -0,0 +1,41 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package purego
5+
6+import "reflect"
7+
8+func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
9+ panic("purego: struct arguments are not supported")
10+}
11+
12+func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
13+ panic("purego: struct returns are not supported")
14+}
15+
16+func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
17+ panic("purego: placeRegisters not implemented on arm")
18+}
19+
20+// shouldBundleStackArgs always returns false on arm
21+// since C-style stack argument bundling is only needed on Darwin ARM64.
22+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
23+ return false
24+}
25+
26+// structFitsInRegisters is not used on arm.
27+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
28+ panic("purego: structFitsInRegisters should not be called on arm")
29+}
30+
31+// collectStackArgs is not used on arm.
32+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
33+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
34+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
35+ panic("purego: collectStackArgs should not be called on arm")
36+}
37+
38+// bundleStackArgs is not used on arm.
39+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
40+ panic("purego: bundleStackArgs should not be called on arm")
41+}
M struct_arm64.go +263 -0
@@ -6,7 +6,12 @@ package purego
66 import (
77 "math"
88 "reflect"
9+ "runtime"
10+ "strconv"
11+ stdstrings "strings"
912 "unsafe"
13+
14+ "github.com/ebitengine/purego/internal/strings"
1015 )
1116
1217 func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
@@ -87,6 +92,14 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
8792 }
8893
8994 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
95+ if runtime.GOOS == "darwin" {
96+ placeRegistersDarwin(v, addFloat, addInt)
97+ return
98+ }
99+ placeRegistersArm64(v, addFloat, addInt)
100+}
101+
102+func placeRegistersArm64(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
90103 var val uint64
91104 var shift byte
92105 var flushed bool
@@ -284,3 +297,253 @@ func isHVA(t reflect.Type) bool {
284297 return false
285298 }
286299 }
300+
301+// copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback.
302+// This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs.
303+func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) {
304+ if runtime.GOOS != "darwin" {
305+ panic("purego: should only be called on darwin")
306+ }
307+ for offset := uintptr(0); offset < size; offset += 8 {
308+ var chunk uintptr
309+ remaining := size - offset
310+ if remaining >= 8 {
311+ chunk = *(*uintptr)(unsafe.Add(ptr, offset))
312+ } else {
313+ // Read byte-by-byte to avoid reading beyond allocation
314+ for i := uintptr(0); i < remaining; i++ {
315+ b := *(*byte)(unsafe.Add(ptr, offset+i))
316+ chunk |= uintptr(b) << (i * 8)
317+ }
318+ }
319+ addChunk(chunk)
320+ }
321+}
322+
323+// placeRegisters implements Darwin ARM64 calling convention for struct arguments.
324+//
325+// For HFA/HVA structs, each element must go in a separate register (or stack slot for elements
326+// that don't fit in registers). We use placeRegistersArm64 for this.
327+//
328+// For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in
329+// 8-byte chunks, which works correctly for both register and stack placement.
330+func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
331+ if runtime.GOOS != "darwin" {
332+ panic("purego: placeRegistersDarwin should only be called on darwin")
333+ }
334+ // Check if this is an HFA/HVA
335+ hfa := isHFA(v.Type())
336+ hva := isHVA(v.Type())
337+
338+ // For HFA/HVA structs, use the standard ARM64 logic which places each element separately
339+ if hfa || hva {
340+ placeRegistersArm64(v, addFloat, addInt)
341+ return
342+ }
343+
344+ // For non-HFA/HVA structs, use byte-level copying
345+ // If the value is not addressable, create an addressable copy
346+ if !v.CanAddr() {
347+ addressable := reflect.New(v.Type()).Elem()
348+ addressable.Set(v)
349+ v = addressable
350+ }
351+ ptr := unsafe.Pointer(v.Addr().Pointer())
352+ size := v.Type().Size()
353+ copyStruct8ByteChunks(ptr, size, addInt)
354+}
355+
356+// shouldBundleStackArgs determines if we need to start C-style packing for
357+// Darwin ARM64 stack arguments. This happens when registers are exhausted.
358+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
359+ if runtime.GOOS != "darwin" {
360+ return false
361+ }
362+
363+ kind := v.Kind()
364+ isFloat := kind == reflect.Float32 || kind == reflect.Float64
365+ isInt := !isFloat && kind != reflect.Struct
366+ primitiveOnStack :=
367+ (isInt && numInts >= numOfIntegerRegisters()) ||
368+ (isFloat && numFloats >= numOfFloatRegisters)
369+ if primitiveOnStack {
370+ return true
371+ }
372+ if kind != reflect.Struct {
373+ return false
374+ }
375+ hfa := isHFA(v.Type())
376+ hva := isHVA(v.Type())
377+ size := v.Type().Size()
378+ eligible := hfa || hva || size <= 16
379+ if !eligible {
380+ return false
381+ }
382+
383+ if hfa {
384+ need := v.NumField()
385+ return numFloats+need > numOfFloatRegisters
386+ }
387+
388+ if hva {
389+ need := v.NumField()
390+ return numInts+need > numOfIntegerRegisters()
391+ }
392+
393+ slotsNeeded := int((size + align8ByteMask) / align8ByteSize)
394+ return numInts+slotsNeeded > numOfIntegerRegisters()
395+}
396+
397+// structFitsInRegisters determines if a struct can still fit in remaining
398+// registers, used during stack argument bundling to decide if a struct
399+// should go through normal register allocation or be bundled with stack args.
400+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
401+ if runtime.GOOS != "darwin" {
402+ panic("purego: structFitsInRegisters should only be called on darwin")
403+ }
404+ hfa := isHFA(val.Type())
405+ hva := isHVA(val.Type())
406+ size := val.Type().Size()
407+
408+ if hfa {
409+ // HFA: check if elements fit in float registers
410+ if tempNumFloats+val.NumField() <= numOfFloatRegisters {
411+ return true, tempNumInts, tempNumFloats + val.NumField()
412+ }
413+ } else if hva {
414+ // HVA: check if elements fit in int registers
415+ if tempNumInts+val.NumField() <= numOfIntegerRegisters() {
416+ return true, tempNumInts + val.NumField(), tempNumFloats
417+ }
418+ } else if size <= 16 {
419+ // Non-HFA/HVA small structs use int registers for byte-packing
420+ slotsNeeded := int((size + align8ByteMask) / align8ByteSize)
421+ if tempNumInts+slotsNeeded <= numOfIntegerRegisters() {
422+ return true, tempNumInts + slotsNeeded, tempNumFloats
423+ }
424+ }
425+
426+ return false, tempNumInts, tempNumFloats
427+}
428+
429+// collectStackArgs separates remaining arguments into those that fit in registers vs those that go on stack.
430+// It returns the stack arguments and processes register arguments through addValue.
431+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
432+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
433+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
434+ if runtime.GOOS != "darwin" {
435+ panic("purego: collectStackArgs should only be called on darwin")
436+ }
437+
438+ var stackArgs []reflect.Value
439+ tempNumInts := numInts
440+ tempNumFloats := numFloats
441+
442+ for j, val := range args[startIdx:] {
443+ // Determine if this argument goes to register or stack
444+ var fitsInRegister bool
445+ var newNumInts, newNumFloats int
446+
447+ if val.Kind() == reflect.Struct {
448+ // Check if struct still fits in remaining registers
449+ fitsInRegister, newNumInts, newNumFloats = structFitsInRegisters(val, tempNumInts, tempNumFloats)
450+ } else {
451+ // Primitive argument
452+ isFloat := val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64
453+ if isFloat {
454+ fitsInRegister = tempNumFloats < numOfFloatRegisters
455+ newNumFloats = tempNumFloats + 1
456+ newNumInts = tempNumInts
457+ } else {
458+ fitsInRegister = tempNumInts < numOfIntegerRegisters()
459+ newNumInts = tempNumInts + 1
460+ newNumFloats = tempNumFloats
461+ }
462+ }
463+
464+ if fitsInRegister {
465+ // Process through normal register allocation
466+ tempNumInts = newNumInts
467+ tempNumFloats = newNumFloats
468+ keepAlive = addValue(val, keepAlive, addInt, addFloat, addStack, pNumInts, pNumFloats, pNumStack)
469+ } else {
470+ // Convert strings to C strings before bundling
471+ if val.Kind() == reflect.String {
472+ ptr := strings.CString(val.String())
473+ keepAlive = append(keepAlive, ptr)
474+ val = reflect.ValueOf(ptr)
475+ args[startIdx+j] = val
476+ }
477+ stackArgs = append(stackArgs, val)
478+ }
479+ }
480+
481+ return stackArgs, keepAlive
482+}
483+
484+const (
485+ paddingFieldPrefix = "Pad"
486+)
487+
488+// bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing.
489+// It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks.
490+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
491+ if runtime.GOOS != "darwin" {
492+ panic("purego: bundleStackArgs should only be called on darwin")
493+ }
494+ if len(stackArgs) == 0 {
495+ return
496+ }
497+
498+ // Build struct fields with proper C alignment and padding
499+ var fields []reflect.StructField
500+ currentOffset := uintptr(0)
501+ fieldIndex := 0
502+
503+ for j, val := range stackArgs {
504+ valSize := val.Type().Size()
505+ valAlign := val.Type().Align()
506+
507+ // ARM64 requires 8-byte alignment for 8-byte or larger structs
508+ if val.Kind() == reflect.Struct && valSize >= 8 {
509+ valAlign = 8
510+ }
511+
512+ // Add padding field if needed for alignment
513+ if currentOffset%uintptr(valAlign) != 0 {
514+ paddingNeeded := uintptr(valAlign) - (currentOffset % uintptr(valAlign))
515+ fields = append(fields, reflect.StructField{
516+ Name: paddingFieldPrefix + strconv.Itoa(fieldIndex),
517+ Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeOf(byte(0))),
518+ })
519+ currentOffset += paddingNeeded
520+ fieldIndex++
521+ }
522+
523+ fields = append(fields, reflect.StructField{
524+ Name: "X" + strconv.Itoa(j),
525+ Type: val.Type(),
526+ })
527+ currentOffset += valSize
528+ fieldIndex++
529+ }
530+
531+ // Create and populate the packed struct
532+ structType := reflect.StructOf(fields)
533+ structInstance := reflect.New(structType).Elem()
534+
535+ // Set values (skip padding fields)
536+ argIndex := 0
537+ for j := 0; j < structInstance.NumField(); j++ {
538+ fieldName := structType.Field(j).Name
539+ if stdstrings.HasPrefix(fieldName, paddingFieldPrefix) {
540+ continue
541+ }
542+ structInstance.Field(j).Set(stackArgs[argIndex])
543+ argIndex++
544+ }
545+
546+ ptr := unsafe.Pointer(structInstance.Addr().Pointer())
547+ size := structType.Size()
548+ copyStruct8ByteChunks(ptr, size, addStack)
549+}
@@ -6,7 +6,12 @@ package purego
6 import (6 import (
7 "math"7 "math"
8 "reflect"8 "reflect"
9+ "runtime"
10+ "strconv"
11+ stdstrings "strings"
9 "unsafe"12 "unsafe"
13+
14+ "github.com/ebitengine/purego/internal/strings"
10 )15 )
11 16
12 func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {17 func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
@@ -87,6 +92,14 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
87 }92 }
88 93
89 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {94 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
95+ if runtime.GOOS == "darwin" {
96+ placeRegistersDarwin(v, addFloat, addInt)
97+ return
98+ }
99+ placeRegistersArm64(v, addFloat, addInt)
100+}
101+
102+func placeRegistersArm64(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
90 var val uint64103 var val uint64
91 var shift byte104 var shift byte
92 var flushed bool105 var flushed bool
@@ -284,3 +297,253 @@ func isHVA(t reflect.Type) bool {
284 return false297 return false
285 }298 }
286 }299 }
300+
301+// copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback.
302+// This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs.
303+func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) {
304+ if runtime.GOOS != "darwin" {
305+ panic("purego: should only be called on darwin")
306+ }
307+ for offset := uintptr(0); offset < size; offset += 8 {
308+ var chunk uintptr
309+ remaining := size - offset
310+ if remaining >= 8 {
311+ chunk = *(*uintptr)(unsafe.Add(ptr, offset))
312+ } else {
313+ // Read byte-by-byte to avoid reading beyond allocation
314+ for i := uintptr(0); i < remaining; i++ {
315+ b := *(*byte)(unsafe.Add(ptr, offset+i))
316+ chunk |= uintptr(b) << (i * 8)
317+ }
318+ }
319+ addChunk(chunk)
320+ }
321+}
322+
323+// placeRegisters implements Darwin ARM64 calling convention for struct arguments.
324+//
325+// For HFA/HVA structs, each element must go in a separate register (or stack slot for elements
326+// that don't fit in registers). We use placeRegistersArm64 for this.
327+//
328+// For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in
329+// 8-byte chunks, which works correctly for both register and stack placement.
330+func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
331+ if runtime.GOOS != "darwin" {
332+ panic("purego: placeRegistersDarwin should only be called on darwin")
333+ }
334+ // Check if this is an HFA/HVA
335+ hfa := isHFA(v.Type())
336+ hva := isHVA(v.Type())
337+
338+ // For HFA/HVA structs, use the standard ARM64 logic which places each element separately
339+ if hfa || hva {
340+ placeRegistersArm64(v, addFloat, addInt)
341+ return
342+ }
343+
344+ // For non-HFA/HVA structs, use byte-level copying
345+ // If the value is not addressable, create an addressable copy
346+ if !v.CanAddr() {
347+ addressable := reflect.New(v.Type()).Elem()
348+ addressable.Set(v)
349+ v = addressable
350+ }
351+ ptr := unsafe.Pointer(v.Addr().Pointer())
352+ size := v.Type().Size()
353+ copyStruct8ByteChunks(ptr, size, addInt)
354+}
355+
356+// shouldBundleStackArgs determines if we need to start C-style packing for
357+// Darwin ARM64 stack arguments. This happens when registers are exhausted.
358+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
359+ if runtime.GOOS != "darwin" {
360+ return false
361+ }
362+
363+ kind := v.Kind()
364+ isFloat := kind == reflect.Float32 || kind == reflect.Float64
365+ isInt := !isFloat && kind != reflect.Struct
366+ primitiveOnStack :=
367+ (isInt && numInts >= numOfIntegerRegisters()) ||
368+ (isFloat && numFloats >= numOfFloatRegisters)
369+ if primitiveOnStack {
370+ return true
371+ }
372+ if kind != reflect.Struct {
373+ return false
374+ }
375+ hfa := isHFA(v.Type())
376+ hva := isHVA(v.Type())
377+ size := v.Type().Size()
378+ eligible := hfa || hva || size <= 16
379+ if !eligible {
380+ return false
381+ }
382+
383+ if hfa {
384+ need := v.NumField()
385+ return numFloats+need > numOfFloatRegisters
386+ }
387+
388+ if hva {
389+ need := v.NumField()
390+ return numInts+need > numOfIntegerRegisters()
391+ }
392+
393+ slotsNeeded := int((size + align8ByteMask) / align8ByteSize)
394+ return numInts+slotsNeeded > numOfIntegerRegisters()
395+}
396+
397+// structFitsInRegisters determines if a struct can still fit in remaining
398+// registers, used during stack argument bundling to decide if a struct
399+// should go through normal register allocation or be bundled with stack args.
400+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
401+ if runtime.GOOS != "darwin" {
402+ panic("purego: structFitsInRegisters should only be called on darwin")
403+ }
404+ hfa := isHFA(val.Type())
405+ hva := isHVA(val.Type())
406+ size := val.Type().Size()
407+
408+ if hfa {
409+ // HFA: check if elements fit in float registers
410+ if tempNumFloats+val.NumField() <= numOfFloatRegisters {
411+ return true, tempNumInts, tempNumFloats + val.NumField()
412+ }
413+ } else if hva {
414+ // HVA: check if elements fit in int registers
415+ if tempNumInts+val.NumField() <= numOfIntegerRegisters() {
416+ return true, tempNumInts + val.NumField(), tempNumFloats
417+ }
418+ } else if size <= 16 {
419+ // Non-HFA/HVA small structs use int registers for byte-packing
420+ slotsNeeded := int((size + align8ByteMask) / align8ByteSize)
421+ if tempNumInts+slotsNeeded <= numOfIntegerRegisters() {
422+ return true, tempNumInts + slotsNeeded, tempNumFloats
423+ }
424+ }
425+
426+ return false, tempNumInts, tempNumFloats
427+}
428+
429+// collectStackArgs separates remaining arguments into those that fit in registers vs those that go on stack.
430+// It returns the stack arguments and processes register arguments through addValue.
431+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
432+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
433+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
434+ if runtime.GOOS != "darwin" {
435+ panic("purego: collectStackArgs should only be called on darwin")
436+ }
437+
438+ var stackArgs []reflect.Value
439+ tempNumInts := numInts
440+ tempNumFloats := numFloats
441+
442+ for j, val := range args[startIdx:] {
443+ // Determine if this argument goes to register or stack
444+ var fitsInRegister bool
445+ var newNumInts, newNumFloats int
446+
447+ if val.Kind() == reflect.Struct {
448+ // Check if struct still fits in remaining registers
449+ fitsInRegister, newNumInts, newNumFloats = structFitsInRegisters(val, tempNumInts, tempNumFloats)
450+ } else {
451+ // Primitive argument
452+ isFloat := val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64
453+ if isFloat {
454+ fitsInRegister = tempNumFloats < numOfFloatRegisters
455+ newNumFloats = tempNumFloats + 1
456+ newNumInts = tempNumInts
457+ } else {
458+ fitsInRegister = tempNumInts < numOfIntegerRegisters()
459+ newNumInts = tempNumInts + 1
460+ newNumFloats = tempNumFloats
461+ }
462+ }
463+
464+ if fitsInRegister {
465+ // Process through normal register allocation
466+ tempNumInts = newNumInts
467+ tempNumFloats = newNumFloats
468+ keepAlive = addValue(val, keepAlive, addInt, addFloat, addStack, pNumInts, pNumFloats, pNumStack)
469+ } else {
470+ // Convert strings to C strings before bundling
471+ if val.Kind() == reflect.String {
472+ ptr := strings.CString(val.String())
473+ keepAlive = append(keepAlive, ptr)
474+ val = reflect.ValueOf(ptr)
475+ args[startIdx+j] = val
476+ }
477+ stackArgs = append(stackArgs, val)
478+ }
479+ }
480+
481+ return stackArgs, keepAlive
482+}
483+
484+const (
485+ paddingFieldPrefix = "Pad"
486+)
487+
488+// bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing.
489+// It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks.
490+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
491+ if runtime.GOOS != "darwin" {
492+ panic("purego: bundleStackArgs should only be called on darwin")
493+ }
494+ if len(stackArgs) == 0 {
495+ return
496+ }
497+
498+ // Build struct fields with proper C alignment and padding
499+ var fields []reflect.StructField
500+ currentOffset := uintptr(0)
501+ fieldIndex := 0
502+
503+ for j, val := range stackArgs {
504+ valSize := val.Type().Size()
505+ valAlign := val.Type().Align()
506+
507+ // ARM64 requires 8-byte alignment for 8-byte or larger structs
508+ if val.Kind() == reflect.Struct && valSize >= 8 {
509+ valAlign = 8
510+ }
511+
512+ // Add padding field if needed for alignment
513+ if currentOffset%uintptr(valAlign) != 0 {
514+ paddingNeeded := uintptr(valAlign) - (currentOffset % uintptr(valAlign))
515+ fields = append(fields, reflect.StructField{
516+ Name: paddingFieldPrefix + strconv.Itoa(fieldIndex),
517+ Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeOf(byte(0))),
518+ })
519+ currentOffset += paddingNeeded
520+ fieldIndex++
521+ }
522+
523+ fields = append(fields, reflect.StructField{
524+ Name: "X" + strconv.Itoa(j),
525+ Type: val.Type(),
526+ })
527+ currentOffset += valSize
528+ fieldIndex++
529+ }
530+
531+ // Create and populate the packed struct
532+ structType := reflect.StructOf(fields)
533+ structInstance := reflect.New(structType).Elem()
534+
535+ // Set values (skip padding fields)
536+ argIndex := 0
537+ for j := 0; j < structInstance.NumField(); j++ {
538+ fieldName := structType.Field(j).Name
539+ if stdstrings.HasPrefix(fieldName, paddingFieldPrefix) {
540+ continue
541+ }
542+ structInstance.Field(j).Set(stackArgs[argIndex])
543+ argIndex++
544+ }
545+
546+ ptr := unsafe.Pointer(structInstance.Addr().Pointer())
547+ size := structType.Size()
548+ copyStruct8ByteChunks(ptr, size, addStack)
549+}
M struct_loong64.go +23 -0
@@ -188,3 +188,26 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
188188 addInt(uintptr(ptr))
189189 return keepAlive
190190 }
191+
192+// shouldBundleStackArgs always returns false on loong64
193+// since C-style stack argument bundling is only needed on Darwin ARM64.
194+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
195+ return false
196+}
197+
198+// structFitsInRegisters is not used on loong64.
199+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
200+ panic("purego: structFitsInRegisters should not be called on loong64")
201+}
202+
203+// collectStackArgs is not used on loong64.
204+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
205+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
206+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
207+ panic("purego: collectStackArgs should not be called on loong64")
208+}
209+
210+// bundleStackArgs is not used on loong64.
211+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
212+ panic("purego: bundleStackArgs should not be called on loong64")
213+}
@@ -188,3 +188,26 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
188 addInt(uintptr(ptr))188 addInt(uintptr(ptr))
189 return keepAlive189 return keepAlive
190 }190 }
191+
192+// shouldBundleStackArgs always returns false on loong64
193+// since C-style stack argument bundling is only needed on Darwin ARM64.
194+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
195+ return false
196+}
197+
198+// structFitsInRegisters is not used on loong64.
199+func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
200+ panic("purego: structFitsInRegisters should not be called on loong64")
201+}
202+
203+// collectStackArgs is not used on loong64.
204+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
205+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
206+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
207+ panic("purego: collectStackArgs should not be called on loong64")
208+}
209+
210+// bundleStackArgs is not used on loong64.
211+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
212+ panic("purego: bundleStackArgs should not be called on loong64")
213+}
D struct_other.go +0 -20
@@ -1,20 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2024 The Ebitengine Authors
3-
4-//go:build !amd64 && !arm64 && !loong64
5-
6-package purego
7-
8-import "reflect"
9-
10-func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
11- panic("purego: struct arguments are not supported")
12-}
13-
14-func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
15- panic("purego: struct returns are not supported")
16-}
17-
18-func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
19- panic("purego: not needed on other platforms")
20-}
@@ -1,20 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2024 The Ebitengine Authors
3-
4-//go:build !amd64 && !arm64 && !loong64
5-
6-package purego
7-
8-import "reflect"
9-
10-func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
11- panic("purego: struct arguments are not supported")
12-}
13-
14-func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
15- panic("purego: struct returns are not supported")
16-}
17-
18-func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
19- panic("purego: not needed on other platforms")
20-}
A syscall_other.go +20 -0
@@ -0,0 +1,20 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+//go:build linux && !amd64 && !arm64 && !loong64
5+
6+package purego
7+
8+// getCallbackStart returns the start address of the callback region.
9+// This platform doesn't support the callback detection mechanism.
10+// TODO: Remove this function once callback tight packing is implemented.
11+func getCallbackStart() uintptr {
12+ panic("purego: getCallbackStart not supported on this platform")
13+}
14+
15+// getMaxCB returns the maximum number of callbacks.
16+// This platform doesn't support the callback detection mechanism.
17+// TODO: Remove this function once callback tight packing is implemented.
18+func getMaxCB() int {
19+ panic("purego: getMaxCB not supported on this platform")
20+}
@@ -0,0 +1,20 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+//go:build linux && !amd64 && !arm64 && !loong64
5+
6+package purego
7+
8+// getCallbackStart returns the start address of the callback region.
9+// This platform doesn't support the callback detection mechanism.
10+// TODO: Remove this function once callback tight packing is implemented.
11+func getCallbackStart() uintptr {
12+ panic("purego: getCallbackStart not supported on this platform")
13+}
14+
15+// getMaxCB returns the maximum number of callbacks.
16+// This platform doesn't support the callback detection mechanism.
17+// TODO: Remove this function once callback tight packing is implemented.
18+func getMaxCB() int {
19+ panic("purego: getMaxCB not supported on this platform")
20+}
M syscall_sysv.go +12 -0
@@ -224,3 +224,15 @@ func callbackasmAddr(i int) uintptr {
224224 }
225225 return callbackasmABI0 + uintptr(i*entrySize)
226226 }
227+
228+// getCallbackStart returns the start address of the callback region.
229+// TODO: Remove this function once callback tight packing is implemented.
230+func getCallbackStart() uintptr {
231+ return callbackasmABI0
232+}
233+
234+// getMaxCB returns the maximum number of callbacks.
235+// TODO: Remove this function once callback tight packing is implemented.
236+func getMaxCB() int {
237+ return maxCB
238+}
@@ -224,3 +224,15 @@ func callbackasmAddr(i int) uintptr {
224 }224 }
225 return callbackasmABI0 + uintptr(i*entrySize)225 return callbackasmABI0 + uintptr(i*entrySize)
226 }226 }
227+
228+// getCallbackStart returns the start address of the callback region.
229+// TODO: Remove this function once callback tight packing is implemented.
230+func getCallbackStart() uintptr {
231+ return callbackasmABI0
232+}
233+
234+// getMaxCB returns the maximum number of callbacks.
235+// TODO: Remove this function once callback tight packing is implemented.
236+func getMaxCB() int {
237+ return maxCB
238+}
M syscall_windows.go +14 -0
@@ -44,3 +44,17 @@ func NewCallback(fn any) uintptr {
4444 func loadSymbol(handle uintptr, name string) (uintptr, error) {
4545 return syscall.GetProcAddress(syscall.Handle(handle), name)
4646 }
47+
48+// getCallbackStart returns the start address of the callback region.
49+// On Windows, this should never be called as tight packing is not used.
50+// TODO: Remove this function once callback tight packing is implemented.
51+func getCallbackStart() uintptr {
52+ panic("purego: getCallbackStart should not be called on Windows")
53+}
54+
55+// getMaxCB returns the maximum number of callbacks.
56+// On Windows, this should never be called as tight packing is not used.
57+// TODO: Remove this function once callback tight packing is implemented.
58+func getMaxCB() int {
59+ panic("purego: getMaxCB should not be called on Windows")
60+}
@@ -44,3 +44,17 @@ func NewCallback(fn any) uintptr {
44 func loadSymbol(handle uintptr, name string) (uintptr, error) {44 func loadSymbol(handle uintptr, name string) (uintptr, error) {
45 return syscall.GetProcAddress(syscall.Handle(handle), name)45 return syscall.GetProcAddress(syscall.Handle(handle), name)
46 }46 }
47+
48+// getCallbackStart returns the start address of the callback region.
49+// On Windows, this should never be called as tight packing is not used.
50+// TODO: Remove this function once callback tight packing is implemented.
51+func getCallbackStart() uintptr {
52+ panic("purego: getCallbackStart should not be called on Windows")
53+}
54+
55+// getMaxCB returns the maximum number of callbacks.
56+// On Windows, this should never be called as tight packing is not used.
57+// TODO: Remove this function once callback tight packing is implemented.
58+func getMaxCB() int {
59+ panic("purego: getMaxCB should not be called on Windows")
60+}
M testdata/abitest/abi_test.c +102 -2
@@ -1,8 +1,10 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2025 The Ebitengine Authors
33
4-#include <stdint.h>
54 #include <assert.h>
5+#include <inttypes.h>
6+#include <stdbool.h>
7+#include <stdint.h>
68 #include <stdio.h>
79 #include <string.h>
810
@@ -10,7 +12,7 @@ uint32_t stack_uint8_t(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t
1012 assert(i == 1);
1113 assert(j == 2);
1214 assert(k == 1024);
13- return a | b | c | d | e | f | g | h | (uint32_t) i | (uint32_t) j | k;
15+ return a | b | c | d | e | f | g | h | (uint32_t)i | (uint32_t)j | k;
1416 }
1517
1618 uint32_t reg_uint8_t(uint8_t a, uint8_t b, uint32_t c) {
@@ -29,3 +31,101 @@ uint32_t stack_string(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e
2931 void stack_8i32_3strings(char* result, size_t size, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char* s1, const char* s2, const char* s3) {
3032 snprintf(result, size, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%s:%s", a1, a2, a3, a4, a5, a6, a7, a8, s1, s2, s3);
3133 }
34+
35+// HFA (Homogeneous Float Aggregate) struct with 2 floats
36+typedef struct {
37+ float x;
38+ float y;
39+} Float2;
40+
41+// HFA struct with 4 floats
42+typedef struct {
43+ float x;
44+ float y;
45+ float z;
46+ float w;
47+} Float4;
48+
49+// Non-HFA struct (mixed types)
50+typedef struct {
51+ int32_t a;
52+ float b;
53+} MixedStruct;
54+
55+// Small struct that fits in one register
56+typedef struct {
57+ int32_t x;
58+ int32_t y;
59+} IntPair;
60+
61+// Test: 8 int registers exhausted, then HFA struct on stack
62+void stack_8int_hfa2_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float2 f) {
63+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f",
64+ a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y);
65+}
66+
67+// Test: 8 int registers exhausted, then multiple structs on stack
68+void stack_8int_2structs_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, IntPair p1, IntPair p2) {
69+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
70+ a1, a2, a3, a4, a5, a6, a7, a8, p1.x, p1.y, p2.x, p2.y);
71+}
72+
73+// Test: 8 float registers exhausted, then HFA on stack
74+void stack_8float_hfa2_stack(char *buf, size_t bufsize, float f1, float f2, float f3, float f4, float f5, float f6, float f7, float f8, Float2 f) {
75+ snprintf(buf, bufsize, "%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f",
76+ f1, f2, f3, f4, f5, f6, f7, f8, f.x, f.y);
77+}
78+
79+// Test: mixed - int regs exhausted, float struct can still use float regs
80+void stack_8int_hfa2_floatregs(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float2 f) {
81+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f",
82+ a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y);
83+}
84+
85+// Test: primitives and struct interleaved on stack
86+void stack_8int_int_struct_int(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, IntPair p, int32_t a10) {
87+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
88+ a1, a2, a3, a4, a5, a6, a7, a8, a9, p.x, p.y, a10);
89+}
90+
91+// Test: HFA4 struct on stack (4 floats)
92+void stack_8int_hfa4_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float4 f) {
93+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f:%.1f:%.1f",
94+ a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y, f.z, f.w);
95+}
96+
97+// Test: mixed type struct on stack
98+void stack_8int_mixed_struct(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, MixedStruct m) {
99+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%.1f",
100+ a1, a2, a3, a4, a5, a6, a7, a8, m.a, m.b);
101+}
102+
103+void stack_10_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10) {
104+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
105+ a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
106+}
107+
108+void stack_11_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10, int32_t a11) {
109+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
110+ a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11);
111+}
112+
113+void stack_10_float32(char *buf, size_t bufsize, float f1, float f2, float f3, float f4, float f5, float f6, float f7, float f8, float f9, float f10) {
114+ snprintf(buf, bufsize, "%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f",
115+ f1, f2, f3, f4, f5, f6, f7, f8, f9, f10);
116+}
117+
118+void stack_mixed_stack_4args(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char *s1, bool b1, int32_t a9, const char *s2) {
119+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%d:%d:%s",
120+ a1, a2, a3, a4, a5, a6, a7, a8, s1, b1, a9, s2);
121+}
122+
123+void stack_20_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10, int32_t a11, int32_t a12, int32_t a13, int32_t a14, int32_t a15, int32_t a16, int32_t a17, int32_t a18, int32_t a19, int32_t a20) {
124+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
125+ a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20);
126+}
127+
128+void stack_25_int64_exceeds(char *buf, size_t bufsize, int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10, int64_t a11, int64_t a12, int64_t a13, int64_t a14, int64_t a15, int64_t a16, int64_t a17, int64_t a18, int64_t a19, int64_t a20, int64_t a21, int64_t a22, int64_t a23, int64_t a24, int64_t a25) {
129+ snprintf(buf, bufsize, "%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64,
130+ a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25);
131+}
@@ -1,8 +1,10 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2025 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3 3
4-#include <stdint.h>
5 #include <assert.h>4 #include <assert.h>
5+#include <inttypes.h>
6+#include <stdbool.h>
7+#include <stdint.h>
6 #include <stdio.h>8 #include <stdio.h>
7 #include <string.h>9 #include <string.h>
8 10
@@ -10,7 +12,7 @@ uint32_t stack_uint8_t(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t
10 assert(i == 1);12 assert(i == 1);
11 assert(j == 2);13 assert(j == 2);
12 assert(k == 1024);14 assert(k == 1024);
13- return a | b | c | d | e | f | g | h | (uint32_t) i | (uint32_t) j | k;15+ return a | b | c | d | e | f | g | h | (uint32_t)i | (uint32_t)j | k;
14 }16 }
15 17
16 uint32_t reg_uint8_t(uint8_t a, uint8_t b, uint32_t c) {18 uint32_t reg_uint8_t(uint8_t a, uint8_t b, uint32_t c) {
@@ -29,3 +31,101 @@ uint32_t stack_string(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e
29 void stack_8i32_3strings(char* result, size_t size, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char* s1, const char* s2, const char* s3) {31 void stack_8i32_3strings(char* result, size_t size, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char* s1, const char* s2, const char* s3) {
30 snprintf(result, size, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%s:%s", a1, a2, a3, a4, a5, a6, a7, a8, s1, s2, s3);32 snprintf(result, size, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%s:%s", a1, a2, a3, a4, a5, a6, a7, a8, s1, s2, s3);
31 }33 }
34+
35+// HFA (Homogeneous Float Aggregate) struct with 2 floats
36+typedef struct {
37+ float x;
38+ float y;
39+} Float2;
40+
41+// HFA struct with 4 floats
42+typedef struct {
43+ float x;
44+ float y;
45+ float z;
46+ float w;
47+} Float4;
48+
49+// Non-HFA struct (mixed types)
50+typedef struct {
51+ int32_t a;
52+ float b;
53+} MixedStruct;
54+
55+// Small struct that fits in one register
56+typedef struct {
57+ int32_t x;
58+ int32_t y;
59+} IntPair;
60+
61+// Test: 8 int registers exhausted, then HFA struct on stack
62+void stack_8int_hfa2_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float2 f) {
63+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f",
64+ a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y);
65+}
66+
67+// Test: 8 int registers exhausted, then multiple structs on stack
68+void stack_8int_2structs_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, IntPair p1, IntPair p2) {
69+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
70+ a1, a2, a3, a4, a5, a6, a7, a8, p1.x, p1.y, p2.x, p2.y);
71+}
72+
73+// Test: 8 float registers exhausted, then HFA on stack
74+void stack_8float_hfa2_stack(char *buf, size_t bufsize, float f1, float f2, float f3, float f4, float f5, float f6, float f7, float f8, Float2 f) {
75+ snprintf(buf, bufsize, "%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f",
76+ f1, f2, f3, f4, f5, f6, f7, f8, f.x, f.y);
77+}
78+
79+// Test: mixed - int regs exhausted, float struct can still use float regs
80+void stack_8int_hfa2_floatregs(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float2 f) {
81+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f",
82+ a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y);
83+}
84+
85+// Test: primitives and struct interleaved on stack
86+void stack_8int_int_struct_int(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, IntPair p, int32_t a10) {
87+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
88+ a1, a2, a3, a4, a5, a6, a7, a8, a9, p.x, p.y, a10);
89+}
90+
91+// Test: HFA4 struct on stack (4 floats)
92+void stack_8int_hfa4_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float4 f) {
93+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f:%.1f:%.1f",
94+ a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y, f.z, f.w);
95+}
96+
97+// Test: mixed type struct on stack
98+void stack_8int_mixed_struct(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, MixedStruct m) {
99+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%.1f",
100+ a1, a2, a3, a4, a5, a6, a7, a8, m.a, m.b);
101+}
102+
103+void stack_10_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10) {
104+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
105+ a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
106+}
107+
108+void stack_11_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10, int32_t a11) {
109+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
110+ a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11);
111+}
112+
113+void stack_10_float32(char *buf, size_t bufsize, float f1, float f2, float f3, float f4, float f5, float f6, float f7, float f8, float f9, float f10) {
114+ snprintf(buf, bufsize, "%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f",
115+ f1, f2, f3, f4, f5, f6, f7, f8, f9, f10);
116+}
117+
118+void stack_mixed_stack_4args(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char *s1, bool b1, int32_t a9, const char *s2) {
119+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%d:%d:%s",
120+ a1, a2, a3, a4, a5, a6, a7, a8, s1, b1, a9, s2);
121+}
122+
123+void stack_20_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10, int32_t a11, int32_t a12, int32_t a13, int32_t a14, int32_t a15, int32_t a16, int32_t a17, int32_t a18, int32_t a19, int32_t a20) {
124+ snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d",
125+ a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20);
126+}
127+
128+void stack_25_int64_exceeds(char *buf, size_t bufsize, int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10, int64_t a11, int64_t a12, int64_t a13, int64_t a14, int64_t a15, int64_t a16, int64_t a17, int64_t a18, int64_t a19, int64_t a20, int64_t a21, int64_t a22, int64_t a23, int64_t a24, int64_t a25) {
129+ snprintf(buf, bufsize, "%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64,
130+ a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25);
131+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close