detached 1d6e403

Implement Darwin ARM64 callback argument packing (#364)

PR #353 introduced tight argument packing for Darwin ARM64 function calls,
allowing efficient stack usage for functions with many arguments. However,
callbacks (C calling into Go) continued using the traditional 8-byte-per-slot
layout, creating an asymmetry in the calling convention.

This PR completes the implementation by adding matching tight packing support
for callbacks.

Note that this contains the commits from #353 and should be rebased once that
PR is in.

Updates #352
commit
1d6e4037a692577a85a3d815014549e122c1dbab
author
Travis Cline · · 2026-01-27 15:53 UTC
committer
GitHub · · 2026-01-27 15:53 UTC
parent
432cdf2
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
6 files · +357 -103
M callback_test.go +233 -0
@@ -9,6 +9,7 @@ import (
99 "fmt"
1010 "os"
1111 "path/filepath"
12+ "runtime"
1213 "testing"
1314 "unsafe"
1415
@@ -173,3 +174,235 @@ func ExampleNewCallback_cdecl() {
173174
174175 // Output: 83
175176 }
177+
178+func TestCallbackInt32Packing(t *testing.T) {
179+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
180+ t.Skip("callback tight packing only applies to darwin/arm64")
181+ }
182+
183+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
184+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
185+ t.Fatal(err)
186+ }
187+ defer os.Remove(libFileName)
188+
189+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
190+ if err != nil {
191+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
192+ }
193+
194+ var callCallback12Int32 func(cb uintptr) int32
195+ purego.RegisterLibFunc(&callCallback12Int32, lib, "callCallback12Int32")
196+
197+ // Go callback that sums the 12 int32 arguments (prime numbers: 2,3,5,7,11,13,17,19,23,29,31,37)
198+ goFunc := func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 int32) int32 {
199+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12
200+ }
201+
202+ cb := purego.NewCallback(goFunc)
203+ got := callCallback12Int32(cb)
204+ want := int32(2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 + 31 + 37) // 197
205+ if got != want {
206+ t.Errorf("callCallback12Int32() = %d, want %d", got, want)
207+ }
208+}
209+
210+func TestCallbackMixedStackPacking(t *testing.T) {
211+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
212+ t.Skip("callback tight packing only applies to darwin/arm64")
213+ }
214+
215+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
216+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
217+ t.Fatal(err)
218+ }
219+ defer os.Remove(libFileName)
220+
221+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
222+ if err != nil {
223+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
224+ }
225+
226+ var callCallbackMixedStack func(cb uintptr) int64
227+ purego.RegisterLibFunc(&callCallbackMixedStack, lib, "callCallbackMixedStack")
228+
229+ // Go callback: 8 int64s in regs, then int32(100), int64(200), int32(300) on stack
230+ goFunc := func(a1, a2, a3, a4, a5, a6, a7, a8 int64, s1 int32, s2 int64, s3 int32) int64 {
231+ // Return sum to verify all args received correctly
232+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + int64(s1) + s2 + int64(s3)
233+ }
234+
235+ cb := purego.NewCallback(goFunc)
236+ got := callCallbackMixedStack(cb)
237+ want := int64(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 100 + 200 + 300) // 636
238+ if got != want {
239+ t.Errorf("callCallbackMixedStack() = %d, want %d", got, want)
240+ }
241+}
242+
243+func TestCallbackSmallTypesPacking(t *testing.T) {
244+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
245+ t.Skip("callback tight packing only applies to darwin/arm64")
246+ }
247+
248+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
249+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
250+ t.Fatal(err)
251+ }
252+ defer os.Remove(libFileName)
253+
254+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
255+ if err != nil {
256+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
257+ }
258+
259+ var callCallbackSmallTypes func(cb uintptr) int64
260+ purego.RegisterLibFunc(&callCallbackSmallTypes, lib, "callCallbackSmallTypes")
261+
262+ // Values: 1,2,3,4,5,6,7,8 (regs), true, -42, 200, -1000, 50000, 123456 (stack)
263+ var gotBool bool
264+ var gotI8 int8
265+ var gotU8 uint8
266+ var gotI16 int16
267+ var gotU16 uint16
268+ var gotI32 int32
269+
270+ goFunc := func(a1, a2, a3, a4, a5, a6, a7, a8 int64,
271+ b bool, i8 int8, u8 uint8, i16 int16, u16 uint16, i32 int32) int64 {
272+ gotBool = b
273+ gotI8 = i8
274+ gotU8 = u8
275+ gotI16 = i16
276+ gotU16 = u16
277+ gotI32 = i32
278+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8
279+ }
280+
281+ cb := purego.NewCallback(goFunc)
282+ got := callCallbackSmallTypes(cb)
283+
284+ // Check register args sum
285+ want := int64(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8) // 36
286+ if got != want {
287+ t.Errorf("register args sum = %d, want %d", got, want)
288+ }
289+
290+ // Check stack args
291+ if gotBool != true {
292+ t.Errorf("bool = %v, want true", gotBool)
293+ }
294+ if gotI8 != -42 {
295+ t.Errorf("int8 = %d, want -42", gotI8)
296+ }
297+ if gotU8 != 200 {
298+ t.Errorf("uint8 = %d, want 200", gotU8)
299+ }
300+ if gotI16 != -1000 {
301+ t.Errorf("int16 = %d, want -1000", gotI16)
302+ }
303+ if gotU16 != 50000 {
304+ t.Errorf("uint16 = %d, want 50000", gotU16)
305+ }
306+ if gotI32 != 123456 {
307+ t.Errorf("int32 = %d, want 123456", gotI32)
308+ }
309+}
310+
311+func TestCallback10Int32Packing(t *testing.T) {
312+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
313+ t.Skip("callback tight packing only applies to darwin/arm64")
314+ }
315+
316+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
317+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
318+ t.Fatal(err)
319+ }
320+ defer os.Remove(libFileName)
321+
322+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
323+ if err != nil {
324+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
325+ }
326+
327+ var callCallback10Int32 func(cb uintptr) int32
328+ purego.RegisterLibFunc(&callCallback10Int32, lib, "callCallback10Int32")
329+
330+ goFunc := func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 int32) int32 {
331+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10
332+ }
333+
334+ cb := purego.NewCallback(goFunc)
335+ got := callCallback10Int32(cb)
336+ want := int32(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10) // 55
337+ if got != want {
338+ t.Errorf("callCallback10Int32() = %d, want %d", got, want)
339+ }
340+}
341+
342+func TestCallbackFloat64StackPacking(t *testing.T) {
343+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
344+ t.Skip("callback tight packing only applies to darwin/arm64")
345+ }
346+
347+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
348+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
349+ t.Fatal(err)
350+ }
351+ defer os.Remove(libFileName)
352+
353+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
354+ if err != nil {
355+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
356+ }
357+
358+ var callCallback10Float64 func(cb uintptr) int64
359+ purego.RegisterLibFunc(&callCallback10Float64, lib, "callCallback10Float64")
360+
361+ // 10 float64s: 8 in registers, 2 on stack
362+ // Return int64 since callbacks don't support float returns
363+ goFunc := func(f1, f2, f3, f4, f5, f6, f7, f8, f9, f10 float64) int64 {
364+ sum := f1 + f2 + f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10
365+ return int64(sum * 10) // 60.0 * 10 = 600
366+ }
367+
368+ cb := purego.NewCallback(goFunc)
369+ got := callCallback10Float64(cb)
370+ want := int64(600)
371+ if got != want {
372+ t.Errorf("callCallback10Float64() = %d, want %d", got, want)
373+ }
374+}
375+
376+func TestCallbackFloat32StackPacking(t *testing.T) {
377+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
378+ t.Skip("callback tight packing only applies to darwin/arm64")
379+ }
380+
381+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
382+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
383+ t.Fatal(err)
384+ }
385+ defer os.Remove(libFileName)
386+
387+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
388+ if err != nil {
389+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
390+ }
391+
392+ var callCallback12Float32 func(cb uintptr) int64
393+ purego.RegisterLibFunc(&callCallback12Float32, lib, "callCallback12Float32")
394+
395+ // 12 float32s: 8 in registers, 4 on stack
396+ // Return int64 since callbacks don't support float returns
397+ goFunc := func(f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12 float32) int64 {
398+ sum := f1 + f2 + f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10 + f11 + f12
399+ return int64(sum) // 78
400+ }
401+
402+ cb := purego.NewCallback(goFunc)
403+ got := callCallback12Float32(cb)
404+ want := int64(78)
405+ if got != want {
406+ t.Errorf("callCallback12Float32() = %d, want %d", got, want)
407+ }
408+}
@@ -9,6 +9,7 @@ import (
9 "fmt"9 "fmt"
10 "os"10 "os"
11 "path/filepath"11 "path/filepath"
12+ "runtime"
12 "testing"13 "testing"
13 "unsafe"14 "unsafe"
14 15
@@ -173,3 +174,235 @@ func ExampleNewCallback_cdecl() {
173 174
174 // Output: 83175 // Output: 83
175 }176 }
177+
178+func TestCallbackInt32Packing(t *testing.T) {
179+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
180+ t.Skip("callback tight packing only applies to darwin/arm64")
181+ }
182+
183+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
184+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
185+ t.Fatal(err)
186+ }
187+ defer os.Remove(libFileName)
188+
189+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
190+ if err != nil {
191+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
192+ }
193+
194+ var callCallback12Int32 func(cb uintptr) int32
195+ purego.RegisterLibFunc(&callCallback12Int32, lib, "callCallback12Int32")
196+
197+ // Go callback that sums the 12 int32 arguments (prime numbers: 2,3,5,7,11,13,17,19,23,29,31,37)
198+ goFunc := func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 int32) int32 {
199+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12
200+ }
201+
202+ cb := purego.NewCallback(goFunc)
203+ got := callCallback12Int32(cb)
204+ want := int32(2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 + 31 + 37) // 197
205+ if got != want {
206+ t.Errorf("callCallback12Int32() = %d, want %d", got, want)
207+ }
208+}
209+
210+func TestCallbackMixedStackPacking(t *testing.T) {
211+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
212+ t.Skip("callback tight packing only applies to darwin/arm64")
213+ }
214+
215+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
216+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
217+ t.Fatal(err)
218+ }
219+ defer os.Remove(libFileName)
220+
221+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
222+ if err != nil {
223+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
224+ }
225+
226+ var callCallbackMixedStack func(cb uintptr) int64
227+ purego.RegisterLibFunc(&callCallbackMixedStack, lib, "callCallbackMixedStack")
228+
229+ // Go callback: 8 int64s in regs, then int32(100), int64(200), int32(300) on stack
230+ goFunc := func(a1, a2, a3, a4, a5, a6, a7, a8 int64, s1 int32, s2 int64, s3 int32) int64 {
231+ // Return sum to verify all args received correctly
232+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + int64(s1) + s2 + int64(s3)
233+ }
234+
235+ cb := purego.NewCallback(goFunc)
236+ got := callCallbackMixedStack(cb)
237+ want := int64(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 100 + 200 + 300) // 636
238+ if got != want {
239+ t.Errorf("callCallbackMixedStack() = %d, want %d", got, want)
240+ }
241+}
242+
243+func TestCallbackSmallTypesPacking(t *testing.T) {
244+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
245+ t.Skip("callback tight packing only applies to darwin/arm64")
246+ }
247+
248+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
249+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
250+ t.Fatal(err)
251+ }
252+ defer os.Remove(libFileName)
253+
254+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
255+ if err != nil {
256+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
257+ }
258+
259+ var callCallbackSmallTypes func(cb uintptr) int64
260+ purego.RegisterLibFunc(&callCallbackSmallTypes, lib, "callCallbackSmallTypes")
261+
262+ // Values: 1,2,3,4,5,6,7,8 (regs), true, -42, 200, -1000, 50000, 123456 (stack)
263+ var gotBool bool
264+ var gotI8 int8
265+ var gotU8 uint8
266+ var gotI16 int16
267+ var gotU16 uint16
268+ var gotI32 int32
269+
270+ goFunc := func(a1, a2, a3, a4, a5, a6, a7, a8 int64,
271+ b bool, i8 int8, u8 uint8, i16 int16, u16 uint16, i32 int32) int64 {
272+ gotBool = b
273+ gotI8 = i8
274+ gotU8 = u8
275+ gotI16 = i16
276+ gotU16 = u16
277+ gotI32 = i32
278+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8
279+ }
280+
281+ cb := purego.NewCallback(goFunc)
282+ got := callCallbackSmallTypes(cb)
283+
284+ // Check register args sum
285+ want := int64(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8) // 36
286+ if got != want {
287+ t.Errorf("register args sum = %d, want %d", got, want)
288+ }
289+
290+ // Check stack args
291+ if gotBool != true {
292+ t.Errorf("bool = %v, want true", gotBool)
293+ }
294+ if gotI8 != -42 {
295+ t.Errorf("int8 = %d, want -42", gotI8)
296+ }
297+ if gotU8 != 200 {
298+ t.Errorf("uint8 = %d, want 200", gotU8)
299+ }
300+ if gotI16 != -1000 {
301+ t.Errorf("int16 = %d, want -1000", gotI16)
302+ }
303+ if gotU16 != 50000 {
304+ t.Errorf("uint16 = %d, want 50000", gotU16)
305+ }
306+ if gotI32 != 123456 {
307+ t.Errorf("int32 = %d, want 123456", gotI32)
308+ }
309+}
310+
311+func TestCallback10Int32Packing(t *testing.T) {
312+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
313+ t.Skip("callback tight packing only applies to darwin/arm64")
314+ }
315+
316+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
317+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
318+ t.Fatal(err)
319+ }
320+ defer os.Remove(libFileName)
321+
322+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
323+ if err != nil {
324+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
325+ }
326+
327+ var callCallback10Int32 func(cb uintptr) int32
328+ purego.RegisterLibFunc(&callCallback10Int32, lib, "callCallback10Int32")
329+
330+ goFunc := func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 int32) int32 {
331+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10
332+ }
333+
334+ cb := purego.NewCallback(goFunc)
335+ got := callCallback10Int32(cb)
336+ want := int32(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10) // 55
337+ if got != want {
338+ t.Errorf("callCallback10Int32() = %d, want %d", got, want)
339+ }
340+}
341+
342+func TestCallbackFloat64StackPacking(t *testing.T) {
343+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
344+ t.Skip("callback tight packing only applies to darwin/arm64")
345+ }
346+
347+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
348+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
349+ t.Fatal(err)
350+ }
351+ defer os.Remove(libFileName)
352+
353+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
354+ if err != nil {
355+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
356+ }
357+
358+ var callCallback10Float64 func(cb uintptr) int64
359+ purego.RegisterLibFunc(&callCallback10Float64, lib, "callCallback10Float64")
360+
361+ // 10 float64s: 8 in registers, 2 on stack
362+ // Return int64 since callbacks don't support float returns
363+ goFunc := func(f1, f2, f3, f4, f5, f6, f7, f8, f9, f10 float64) int64 {
364+ sum := f1 + f2 + f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10
365+ return int64(sum * 10) // 60.0 * 10 = 600
366+ }
367+
368+ cb := purego.NewCallback(goFunc)
369+ got := callCallback10Float64(cb)
370+ want := int64(600)
371+ if got != want {
372+ t.Errorf("callCallback10Float64() = %d, want %d", got, want)
373+ }
374+}
375+
376+func TestCallbackFloat32StackPacking(t *testing.T) {
377+ if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
378+ t.Skip("callback tight packing only applies to darwin/arm64")
379+ }
380+
381+ libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
382+ if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
383+ t.Fatal(err)
384+ }
385+ defer os.Remove(libFileName)
386+
387+ lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
388+ if err != nil {
389+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
390+ }
391+
392+ var callCallback12Float32 func(cb uintptr) int64
393+ purego.RegisterLibFunc(&callCallback12Float32, lib, "callCallback12Float32")
394+
395+ // 12 float32s: 8 in registers, 4 on stack
396+ // Return int64 since callbacks don't support float returns
397+ goFunc := func(f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12 float32) int64 {
398+ sum := f1 + f2 + f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10 + f11 + f12
399+ return int64(sum) // 78
400+ }
401+
402+ cb := purego.NewCallback(goFunc)
403+ got := callCallback12Float32(cb)
404+ want := int64(78)
405+ if got != want {
406+ t.Errorf("callCallback12Float32() = %d, want %d", got, want)
407+ }
408+}
M func.go +5 -43
@@ -210,7 +210,8 @@ func RegisterFunc(fptr any, cfn uintptr) {
210210 }
211211
212212 sizeOfStack := maxArgs - numOfIntegerRegisters()
213- // On Darwin ARM64, use byte-based validation since arguments pack efficiently
213+ // On Darwin ARM64, use byte-based validation since arguments pack efficiently.
214+ // See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
214215 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
215216 stackBytes := estimateStackBytes(ty)
216217 maxStackBytes := sizeOfStack * 8
@@ -224,11 +225,6 @@ func RegisterFunc(fptr any, cfn uintptr) {
224225 }
225226 }
226227
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-
232228 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
233229 var sysargs [maxArgs]uintptr
234230 var floats [numOfFloatRegisters]uintptr
@@ -305,9 +301,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
305301 continue
306302 }
307303 // 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) {
304+ if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
311305 // Collect and separate remaining args into register vs stack
312306 stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
313307 keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
@@ -504,8 +498,8 @@ func numOfIntegerRegisters() int {
504498 // estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.
505499 // This is a conservative estimate used only for early error detection.
506500 func estimateStackBytes(ty reflect.Type) int {
507- numInts, numFloats := 0, 0
508- stackBytes := 0
501+ var numInts, numFloats int
502+ var stackBytes int
509503
510504 for i := 0; i < ty.NumIn(); i++ {
511505 arg := ty.In(i)
@@ -528,35 +522,3 @@ func estimateStackBytes(ty reflect.Type) int {
528522 }
529523 return stackBytes
530524 }
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 || riscv64)) || 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" || runtime.GOARCH == "riscv64"))
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", "riscv64":
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-}
@@ -210,7 +210,8 @@ func RegisterFunc(fptr any, cfn uintptr) {
210 }210 }
211 211
212 sizeOfStack := maxArgs - numOfIntegerRegisters()212 sizeOfStack := maxArgs - numOfIntegerRegisters()
213- // On Darwin ARM64, use byte-based validation since arguments pack efficiently213+ // On Darwin ARM64, use byte-based validation since arguments pack efficiently.
214+ // See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
214 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {215 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
215 stackBytes := estimateStackBytes(ty)216 stackBytes := estimateStackBytes(ty)
216 maxStackBytes := sizeOfStack * 8217 maxStackBytes := sizeOfStack * 8
@@ -224,11 +225,6 @@ func RegisterFunc(fptr any, cfn uintptr) {
224 }225 }
225 }226 }
226 227
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-
232 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {228 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
233 var sysargs [maxArgs]uintptr229 var sysargs [maxArgs]uintptr
234 var floats [numOfFloatRegisters]uintptr230 var floats [numOfFloatRegisters]uintptr
@@ -305,9 +301,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
305 continue301 continue
306 }302 }
307 // Check if we need to start Darwin ARM64 C-style stack packing303 // 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 unpacking304+ if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
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 stack305 // Collect and separate remaining args into register vs stack
312 stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,306 stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
313 keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)307 keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
@@ -504,8 +498,8 @@ func numOfIntegerRegisters() int {
504 // estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.498 // estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.
505 // This is a conservative estimate used only for early error detection.499 // This is a conservative estimate used only for early error detection.
506 func estimateStackBytes(ty reflect.Type) int {500 func estimateStackBytes(ty reflect.Type) int {
507- numInts, numFloats := 0, 0501+ var numInts, numFloats int
508- stackBytes := 0502+ var stackBytes int
509 503
510 for i := 0; i < ty.NumIn(); i++ {504 for i := 0; i < ty.NumIn(); i++ {
511 arg := ty.In(i)505 arg := ty.In(i)
@@ -528,35 +522,3 @@ func estimateStackBytes(ty reflect.Type) int {
528 }522 }
529 return stackBytes523 return stackBytes
530 }524 }
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 || riscv64)) || 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" || runtime.GOARCH == "riscv64"))
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", "riscv64":
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-}
D syscall_other.go +0 -20
@@ -1,20 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3-
4-//go:build linux && !amd64 && !arm64 && !loong64 && !riscv64
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-}
@@ -1,20 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3-
4-//go:build linux && !amd64 && !arm64 && !loong64 && !riscv64
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 +48 -26
@@ -147,36 +147,47 @@ func callbackWrap(a *callbackArgs) {
147147 frame := (*[callbackMaxFrame]uintptr)(a.args)
148148 var floatsN int // floatsN represents the number of float arguments processed
149149 var intsN int // intsN represents the number of integer arguments processed
150- // stack points to the index into frame of the current stack element.
150+ // stackSlot points to the index into frame of the current stack element.
151151 // The stack begins after the float and integer registers.
152- stack := numOfIntegerRegisters() + numOfFloatRegisters
152+ stackSlot := numOfIntegerRegisters() + numOfFloatRegisters
153+ // stackByteOffset tracks the byte offset within the stack area for Darwin ARM64
154+ // tight packing. On Darwin ARM64, C passes small types packed on the stack.
155+ stackByteOffset := uintptr(0)
153156 for i := range args {
154- var pos int
155- switch fnType.In(i).Kind() {
157+ inType := fnType.In(i)
158+ switch inType.Kind() {
156159 case reflect.Float32, reflect.Float64:
157160 if floatsN >= numOfFloatRegisters {
158- pos = stack
159- stack++
161+ if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
162+ // Darwin ARM64: read from packed stack with proper alignment
163+ args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
164+ } else {
165+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
166+ stackSlot++
167+ }
160168 } else {
161- pos = floatsN
169+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
162170 }
163171 floatsN++
164172 case reflect.Struct:
165173 // This is the CDecl field
166- args[i] = reflect.Zero(fnType.In(i))
167- continue
174+ args[i] = reflect.Zero(inType)
168175 default:
169-
170176 if intsN >= numOfIntegerRegisters() {
171- pos = stack
172- stack++
177+ if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
178+ // Darwin ARM64: read from packed stack with proper alignment
179+ args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
180+ } else {
181+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
182+ stackSlot++
183+ }
173184 } else {
174185 // the integers begin after the floats in frame
175- pos = intsN + numOfFloatRegisters
186+ pos := intsN + numOfFloatRegisters
187+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
176188 }
177189 intsN++
178190 }
179- args[i] = reflect.NewAt(fnType.In(i), unsafe.Pointer(&frame[pos])).Elem()
180191 }
181192 ret := fn.Call(args)
182193 if len(ret) > 0 {
@@ -201,6 +212,29 @@ func callbackWrap(a *callbackArgs) {
201212 }
202213 }
203214
215+// callbackArgFromStack reads an argument from the tightly-packed stack area on Darwin ARM64.
216+// The C ABI on Darwin ARM64 packs small types on the stack without padding to 8 bytes.
217+// This function handles proper alignment and advances stackByteOffset accordingly.
218+func callbackArgFromStack(argsBase unsafe.Pointer, stackSlot int, stackByteOffset *uintptr, inType reflect.Type) reflect.Value {
219+ // Calculate base address of stack area (after float and int registers)
220+ stackBase := unsafe.Add(argsBase, stackSlot*int(ptrSize))
221+
222+ // Get type's natural alignment
223+ align := uintptr(inType.Align())
224+ size := inType.Size()
225+
226+ // Align the offset
227+ if *stackByteOffset%align != 0 {
228+ *stackByteOffset = (*stackByteOffset + align - 1) &^ (align - 1)
229+ }
230+
231+ // Read value at aligned offset
232+ ptr := unsafe.Add(stackBase, *stackByteOffset)
233+ *stackByteOffset += size
234+
235+ return reflect.NewAt(inType, ptr).Elem()
236+}
237+
204238 // callbackasmAddr returns address of runtime.callbackasm
205239 // function adjusted by i.
206240 // On x86 and amd64, runtime.callbackasm is a series of CALL instructions,
@@ -224,15 +258,3 @@ func callbackasmAddr(i int) uintptr {
224258 }
225259 return callbackasmABI0 + uintptr(i*entrySize)
226260 }
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-}
@@ -147,36 +147,47 @@ func callbackWrap(a *callbackArgs) {
147 frame := (*[callbackMaxFrame]uintptr)(a.args)147 frame := (*[callbackMaxFrame]uintptr)(a.args)
148 var floatsN int // floatsN represents the number of float arguments processed148 var floatsN int // floatsN represents the number of float arguments processed
149 var intsN int // intsN represents the number of integer arguments processed149 var intsN int // intsN represents the number of integer arguments processed
150- // stack points to the index into frame of the current stack element.150+ // stackSlot points to the index into frame of the current stack element.
151 // The stack begins after the float and integer registers.151 // The stack begins after the float and integer registers.
152- stack := numOfIntegerRegisters() + numOfFloatRegisters152+ stackSlot := numOfIntegerRegisters() + numOfFloatRegisters
153+ // stackByteOffset tracks the byte offset within the stack area for Darwin ARM64
154+ // tight packing. On Darwin ARM64, C passes small types packed on the stack.
155+ stackByteOffset := uintptr(0)
153 for i := range args {156 for i := range args {
154- var pos int157+ inType := fnType.In(i)
155- switch fnType.In(i).Kind() {158+ switch inType.Kind() {
156 case reflect.Float32, reflect.Float64:159 case reflect.Float32, reflect.Float64:
157 if floatsN >= numOfFloatRegisters {160 if floatsN >= numOfFloatRegisters {
158- pos = stack161+ if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
159- stack++162+ // Darwin ARM64: read from packed stack with proper alignment
163+ args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
164+ } else {
165+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
166+ stackSlot++
167+ }
160 } else {168 } else {
161- pos = floatsN169+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
162 }170 }
163 floatsN++171 floatsN++
164 case reflect.Struct:172 case reflect.Struct:
165 // This is the CDecl field173 // This is the CDecl field
166- args[i] = reflect.Zero(fnType.In(i))174+ args[i] = reflect.Zero(inType)
167- continue
168 default:175 default:
169-
170 if intsN >= numOfIntegerRegisters() {176 if intsN >= numOfIntegerRegisters() {
171- pos = stack177+ if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
172- stack++178+ // Darwin ARM64: read from packed stack with proper alignment
179+ args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
180+ } else {
181+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
182+ stackSlot++
183+ }
173 } else {184 } else {
174 // the integers begin after the floats in frame185 // the integers begin after the floats in frame
175- pos = intsN + numOfFloatRegisters186+ pos := intsN + numOfFloatRegisters
187+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
176 }188 }
177 intsN++189 intsN++
178 }190 }
179- args[i] = reflect.NewAt(fnType.In(i), unsafe.Pointer(&frame[pos])).Elem()
180 }191 }
181 ret := fn.Call(args)192 ret := fn.Call(args)
182 if len(ret) > 0 {193 if len(ret) > 0 {
@@ -201,6 +212,29 @@ func callbackWrap(a *callbackArgs) {
201 }212 }
202 }213 }
203 214
215+// callbackArgFromStack reads an argument from the tightly-packed stack area on Darwin ARM64.
216+// The C ABI on Darwin ARM64 packs small types on the stack without padding to 8 bytes.
217+// This function handles proper alignment and advances stackByteOffset accordingly.
218+func callbackArgFromStack(argsBase unsafe.Pointer, stackSlot int, stackByteOffset *uintptr, inType reflect.Type) reflect.Value {
219+ // Calculate base address of stack area (after float and int registers)
220+ stackBase := unsafe.Add(argsBase, stackSlot*int(ptrSize))
221+
222+ // Get type's natural alignment
223+ align := uintptr(inType.Align())
224+ size := inType.Size()
225+
226+ // Align the offset
227+ if *stackByteOffset%align != 0 {
228+ *stackByteOffset = (*stackByteOffset + align - 1) &^ (align - 1)
229+ }
230+
231+ // Read value at aligned offset
232+ ptr := unsafe.Add(stackBase, *stackByteOffset)
233+ *stackByteOffset += size
234+
235+ return reflect.NewAt(inType, ptr).Elem()
236+}
237+
204 // callbackasmAddr returns address of runtime.callbackasm238 // callbackasmAddr returns address of runtime.callbackasm
205 // function adjusted by i.239 // function adjusted by i.
206 // On x86 and amd64, runtime.callbackasm is a series of CALL instructions,240 // On x86 and amd64, runtime.callbackasm is a series of CALL instructions,
@@ -224,15 +258,3 @@ func callbackasmAddr(i int) uintptr {
224 }258 }
225 return callbackasmABI0 + uintptr(i*entrySize)259 return callbackasmABI0 + uintptr(i*entrySize)
226 }260 }
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 +0 -14
@@ -44,17 +44,3 @@ 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,17 +44,3 @@ 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-}
A testdata/libcbtest/callback_packing_test.c +71 -0
@@ -0,0 +1,71 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#include <stdint.h>
5+#include <stdbool.h>
6+
7+// Test: 12 int32 arguments - fills 8 int registers, then 4 go to stack
8+// With tight packing, the 4 stack args pack into 16 bytes (4x4)
9+// Without tight packing, they would use 32 bytes (4x8)
10+typedef int32_t (*callback_12_int32)(int32_t, int32_t, int32_t, int32_t,
11+ int32_t, int32_t, int32_t, int32_t,
12+ int32_t, int32_t, int32_t, int32_t);
13+
14+int32_t callCallback12Int32(const void *fp) {
15+ // Using prime numbers to detect argument misalignment
16+ return ((callback_12_int32)(fp))(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37);
17+}
18+
19+// Test: mixed int64 and int32 arguments with stack spillover
20+// 8 int64s fill registers, then int32/int64/int32 go to stack
21+// Tests alignment handling when mixing sizes on stack
22+typedef int64_t (*callback_mixed_stack)(int64_t, int64_t, int64_t, int64_t,
23+ int64_t, int64_t, int64_t, int64_t,
24+ int32_t, int64_t, int32_t);
25+
26+int64_t callCallbackMixedStack(const void *fp) {
27+ return ((callback_mixed_stack)(fp))(1, 2, 3, 4, 5, 6, 7, 8, 100, 200, 300);
28+}
29+
30+// Test: various small types on stack
31+// 8 int64s fill registers, then bool/int8/uint8/int16/uint16/int32 go to stack
32+// Tests byte-level packing of different small types
33+typedef int64_t (*callback_small_types)(int64_t, int64_t, int64_t, int64_t,
34+ int64_t, int64_t, int64_t, int64_t,
35+ bool, int8_t, uint8_t, int16_t, uint16_t, int32_t);
36+
37+int64_t callCallbackSmallTypes(const void *fp) {
38+ return ((callback_small_types)(fp))(1, 2, 3, 4, 5, 6, 7, 8,
39+ true, -42, 200, -1000, 50000, 123456);
40+}
41+
42+// Test: 10 int32 arguments - simpler case
43+// 8 fill registers, 2 go to stack (8 bytes packed vs 16 unpacked)
44+typedef int32_t (*callback_10_int32)(int32_t, int32_t, int32_t, int32_t,
45+ int32_t, int32_t, int32_t, int32_t,
46+ int32_t, int32_t);
47+
48+int32_t callCallback10Int32(const void *fp) {
49+ return ((callback_10_int32)(fp))(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
50+}
51+
52+// Test: 10 float64 arguments - fills 8 float registers, then 2 go to stack
53+// Callback returns int64 since purego callbacks don't support float returns
54+typedef int64_t (*callback_10_float64)(double, double, double, double,
55+ double, double, double, double,
56+ double, double);
57+
58+int64_t callCallback10Float64(const void *fp) {
59+ return ((callback_10_float64)(fp))(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5);
60+}
61+
62+// Test: 12 float32 arguments - fills 8 float registers, then 4 go to stack
63+// Callback returns int64 since purego callbacks don't support float returns
64+typedef int64_t (*callback_12_float32)(float, float, float, float,
65+ float, float, float, float,
66+ float, float, float, float);
67+
68+int64_t callCallback12Float32(const void *fp) {
69+ return ((callback_12_float32)(fp))(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f,
70+ 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f);
71+}
@@ -0,0 +1,71 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#include <stdint.h>
5+#include <stdbool.h>
6+
7+// Test: 12 int32 arguments - fills 8 int registers, then 4 go to stack
8+// With tight packing, the 4 stack args pack into 16 bytes (4x4)
9+// Without tight packing, they would use 32 bytes (4x8)
10+typedef int32_t (*callback_12_int32)(int32_t, int32_t, int32_t, int32_t,
11+ int32_t, int32_t, int32_t, int32_t,
12+ int32_t, int32_t, int32_t, int32_t);
13+
14+int32_t callCallback12Int32(const void *fp) {
15+ // Using prime numbers to detect argument misalignment
16+ return ((callback_12_int32)(fp))(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37);
17+}
18+
19+// Test: mixed int64 and int32 arguments with stack spillover
20+// 8 int64s fill registers, then int32/int64/int32 go to stack
21+// Tests alignment handling when mixing sizes on stack
22+typedef int64_t (*callback_mixed_stack)(int64_t, int64_t, int64_t, int64_t,
23+ int64_t, int64_t, int64_t, int64_t,
24+ int32_t, int64_t, int32_t);
25+
26+int64_t callCallbackMixedStack(const void *fp) {
27+ return ((callback_mixed_stack)(fp))(1, 2, 3, 4, 5, 6, 7, 8, 100, 200, 300);
28+}
29+
30+// Test: various small types on stack
31+// 8 int64s fill registers, then bool/int8/uint8/int16/uint16/int32 go to stack
32+// Tests byte-level packing of different small types
33+typedef int64_t (*callback_small_types)(int64_t, int64_t, int64_t, int64_t,
34+ int64_t, int64_t, int64_t, int64_t,
35+ bool, int8_t, uint8_t, int16_t, uint16_t, int32_t);
36+
37+int64_t callCallbackSmallTypes(const void *fp) {
38+ return ((callback_small_types)(fp))(1, 2, 3, 4, 5, 6, 7, 8,
39+ true, -42, 200, -1000, 50000, 123456);
40+}
41+
42+// Test: 10 int32 arguments - simpler case
43+// 8 fill registers, 2 go to stack (8 bytes packed vs 16 unpacked)
44+typedef int32_t (*callback_10_int32)(int32_t, int32_t, int32_t, int32_t,
45+ int32_t, int32_t, int32_t, int32_t,
46+ int32_t, int32_t);
47+
48+int32_t callCallback10Int32(const void *fp) {
49+ return ((callback_10_int32)(fp))(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
50+}
51+
52+// Test: 10 float64 arguments - fills 8 float registers, then 2 go to stack
53+// Callback returns int64 since purego callbacks don't support float returns
54+typedef int64_t (*callback_10_float64)(double, double, double, double,
55+ double, double, double, double,
56+ double, double);
57+
58+int64_t callCallback10Float64(const void *fp) {
59+ return ((callback_10_float64)(fp))(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5);
60+}
61+
62+// Test: 12 float32 arguments - fills 8 float registers, then 4 go to stack
63+// Callback returns int64 since purego callbacks don't support float returns
64+typedef int64_t (*callback_12_float32)(float, float, float, float,
65+ float, float, float, float,
66+ float, float, float, float);
67+
68+int64_t callCallback12Float32(const void *fp) {
69+ return ((callback_12_float32)(fp))(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f,
70+ 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f);
71+}

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