Skip to content

Commit 2a73c27

Browse files
committed
syscall: implement C-packed callback support for Darwin ARM64
Implement proper C-packed argument layout in the callback wrapper, allowing callbacks to correctly handle packed stack arguments on Darwin ARM64. Changes: - Split callbackWrap into platform-specific versions - callbackWrapDarwinARM64: handles C-packed stack layout with natural alignment for Darwin ARM64 - callbackWrapGeneric: maintains 8-byte slot layout for other platforms - Add comprehensive tests for int32, mixed types, and small types - Test both Go→C→Go callback paths with real C code This enables callbacks to work properly with C-packed arguments, fixing argument misalignment issues on Darwin ARM64.
1 parent ad6bcc1 commit 2a73c27

File tree

6 files changed

+251
-35
lines changed

6 files changed

+251
-35
lines changed

callback_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,151 @@ func ExampleNewCallback_cdecl() {
173173

174174
// Output: 83
175175
}
176+
177+
func TestNewCallbackInt32Packing(t *testing.T) {
178+
var result int32
179+
cb := purego.NewCallback(func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 int32) int32 {
180+
result = a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12
181+
return result
182+
})
183+
184+
var fn func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 int32) int32
185+
purego.RegisterFunc(&fn, cb)
186+
187+
got := fn(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37)
188+
want := int32(197)
189+
190+
if got != want {
191+
t.Errorf("callback returned %d, want %d", got, want)
192+
}
193+
}
194+
195+
func TestNewCallbackMixedPacking(t *testing.T) {
196+
var gotI32_1, gotI32_2 int32
197+
var gotI64 int64
198+
cb := purego.NewCallback(func(r1, r2, r3, r4, r5, r6, r7, r8 int64, s1 int32, s2 int64, s3 int32) {
199+
gotI32_1 = s1
200+
gotI64 = s2
201+
gotI32_2 = s3
202+
})
203+
204+
var fn func(r1, r2, r3, r4, r5, r6, r7, r8 int64, s1 int32, s2 int64, s3 int32)
205+
purego.RegisterFunc(&fn, cb)
206+
207+
fn(1, 2, 3, 4, 5, 6, 7, 8, 100, 200, 300)
208+
209+
if gotI32_1 != 100 || gotI64 != 200 || gotI32_2 != 300 {
210+
t.Errorf("got (%d, %d, %d), want (100, 200, 300)", gotI32_1, gotI64, gotI32_2)
211+
}
212+
}
213+
214+
func TestNewCallbackSmallTypes(t *testing.T) {
215+
var gotBool bool
216+
var gotI8 int8
217+
var gotU8 uint8
218+
var gotI16 int16
219+
var gotU16 uint16
220+
var gotI32 int32
221+
cb := purego.NewCallback(func(r1, r2, r3, r4, r5, r6, r7, r8 int64, b bool, i8 int8, u8 uint8, i16 int16, u16 uint16, i32 int32) {
222+
gotBool = b
223+
gotI8 = i8
224+
gotU8 = u8
225+
gotI16 = i16
226+
gotU16 = u16
227+
gotI32 = i32
228+
})
229+
230+
var fn func(r1, r2, r3, r4, r5, r6, r7, r8 int64, b bool, i8 int8, u8 uint8, i16 int16, u16 uint16, i32 int32)
231+
purego.RegisterFunc(&fn, cb)
232+
233+
fn(1, 2, 3, 4, 5, 6, 7, 8, true, -42, 200, -1000, 50000, 123456)
234+
235+
if !gotBool || gotI8 != -42 || gotU8 != 200 || gotI16 != -1000 || gotU16 != 50000 || gotI32 != 123456 {
236+
t.Errorf("got (bool=%v, i8=%d, u8=%d, i16=%d, u16=%d, i32=%d), want (true, -42, 200, -1000, 50000, 123456)",
237+
gotBool, gotI8, gotU8, gotI16, gotU16, gotI32)
238+
}
239+
}
240+
241+
func TestCallbackFromC(t *testing.T) {
242+
libFileName := filepath.Join(t.TempDir(), "libcbpackingtest.so")
243+
244+
if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
245+
t.Fatal(err)
246+
}
247+
defer os.Remove(libFileName)
248+
249+
lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
250+
if err != nil {
251+
t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
252+
}
253+
254+
t.Run("int32_packing", func(t *testing.T) {
255+
var result int32
256+
goCallback := func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 int32) int32 {
257+
result = a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12
258+
return result
259+
}
260+
261+
var callCallbackInt32Packing func(uintptr) int32
262+
purego.RegisterLibFunc(&callCallbackInt32Packing, lib, "callCallbackInt32Packing")
263+
264+
cb := purego.NewCallback(goCallback)
265+
got := callCallbackInt32Packing(cb)
266+
want := int32(197) // sum of primes: 2+3+5+7+11+13+17+19+23+29+31+37
267+
268+
if got != want {
269+
t.Errorf("C called callback returned %d, want %d", got, want)
270+
}
271+
if result != want {
272+
t.Errorf("callback received wrong args, sum=%d, want %d", result, want)
273+
}
274+
})
275+
276+
t.Run("mixed_packing", func(t *testing.T) {
277+
var gotI32_1, gotI32_2 int32
278+
var gotI64 int64
279+
goCallback := func(r1, r2, r3, r4, r5, r6, r7, r8 int64, s1 int32, s2 int64, s3 int32) {
280+
gotI32_1 = s1
281+
gotI64 = s2
282+
gotI32_2 = s3
283+
}
284+
285+
var callCallbackMixedPacking func(uintptr)
286+
purego.RegisterLibFunc(&callCallbackMixedPacking, lib, "callCallbackMixedPacking")
287+
288+
cb := purego.NewCallback(goCallback)
289+
callCallbackMixedPacking(cb)
290+
291+
if gotI32_1 != 100 || gotI64 != 200 || gotI32_2 != 300 {
292+
t.Errorf("callback received (%d, %d, %d), want (100, 200, 300)", gotI32_1, gotI64, gotI32_2)
293+
}
294+
})
295+
296+
t.Run("small_types", func(t *testing.T) {
297+
var gotBool bool
298+
var gotI8 int8
299+
var gotU8 uint8
300+
var gotI16 int16
301+
var gotU16 uint16
302+
var gotI32 int32
303+
goCallback := func(r1, r2, r3, r4, r5, r6, r7, r8 int64, b bool, i8 int8, u8 uint8, i16 int16, u16 uint16, i32 int32) {
304+
gotBool = b
305+
gotI8 = i8
306+
gotU8 = u8
307+
gotI16 = i16
308+
gotU16 = u16
309+
gotI32 = i32
310+
}
311+
312+
var callCallbackSmallTypes func(uintptr)
313+
purego.RegisterLibFunc(&callCallbackSmallTypes, lib, "callCallbackSmallTypes")
314+
315+
cb := purego.NewCallback(goCallback)
316+
callCallbackSmallTypes(cb)
317+
318+
if !gotBool || gotI8 != -42 || gotU8 != 200 || gotI16 != -1000 || gotU16 != 50000 || gotI32 != 123456 {
319+
t.Errorf("callback received (bool=%v, i8=%d, u8=%d, i16=%d, u16=%d, i32=%d), want (true, -42, 200, -1000, 50000, 123456)",
320+
gotBool, gotI8, gotU8, gotI16, gotU16, gotI32)
321+
}
322+
})
323+
}

func.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,12 +390,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
390390
isInt := !isFloat && v.Kind() != reflect.Struct
391391
wouldBeOnStack := (isInt && numInts >= numOfIntegerRegisters()) || (isFloat && numFloats >= numOfFloatRegisters)
392392

393-
// Don't use C struct packing when calling callbacks, as the callback wrapper
394-
// expects the old unpacked layout where each argument is in its own 8-byte slot.
395-
// TODO: Fix the callback wrapper to handle C-packed layout correctly.
396-
isCallback := isCallbackFunction(cfn)
397-
398-
if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && wouldBeOnStack && !isCallback {
393+
if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && wouldBeOnStack {
399394
// Collect remaining arguments that will go on the stack.
400395
// We need to bundle them into a struct to get proper C packing.
401396
var stackArgs []reflect.Value

syscall_cgo_linux.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,3 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
1919
func NewCallback(_ any) uintptr {
2020
panic("purego: NewCallback on Linux is only supported on amd64/arm64/loong64")
2121
}
22-
23-
func isCallbackFunction(cfn uintptr) bool {
24-
return false
25-
}

syscall_sysv.go

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,6 @@ const ptrSize = unsafe.Sizeof((*int)(nil))
124124

125125
const callbackMaxFrame = 64 * ptrSize
126126

127-
func isCallbackFunction(cfn uintptr) bool {
128-
return cfn >= callbackasmABI0 && cfn < callbackasmABI0+uintptr(callbackMaxFrame*8)
129-
}
130-
131127
// callbackasm is implemented in zcallback_GOOS_GOARCH.s
132128
//
133129
//go:linkname __callbackasm callbackasm
@@ -148,6 +144,38 @@ func callbackWrap(a *callbackArgs) {
148144
cbs.lock.Unlock()
149145
fnType := fn.Type()
150146
args := make([]reflect.Value, fnType.NumIn())
147+
148+
if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" {
149+
callbackWrapDarwinARM64(a, fn, fnType, args)
150+
} else {
151+
callbackWrapGeneric(a, fn, fnType, args)
152+
}
153+
154+
ret := fn.Call(args)
155+
if len(ret) > 0 {
156+
switch k := ret[0].Kind(); k {
157+
case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uintptr:
158+
a.result = uintptr(ret[0].Uint())
159+
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
160+
a.result = uintptr(ret[0].Int())
161+
case reflect.Bool:
162+
if ret[0].Bool() {
163+
a.result = 1
164+
} else {
165+
a.result = 0
166+
}
167+
case reflect.Pointer:
168+
a.result = ret[0].Pointer()
169+
case reflect.UnsafePointer:
170+
a.result = ret[0].Pointer()
171+
default:
172+
panic("purego: unsupported kind: " + k.String())
173+
}
174+
}
175+
}
176+
177+
// callbackWrapGeneric handles callbacks with unpacked (8-byte slot) argument layout.
178+
func callbackWrapGeneric(a *callbackArgs, fn reflect.Value, fnType reflect.Type, args []reflect.Value) {
151179
frame := (*[callbackMaxFrame]uintptr)(a.args)
152180
var floatsN int // floatsN represents the number of float arguments processed
153181
var intsN int // intsN represents the number of integer arguments processed
@@ -170,7 +198,6 @@ func callbackWrap(a *callbackArgs) {
170198
args[i] = reflect.Zero(fnType.In(i))
171199
continue
172200
default:
173-
174201
if intsN >= numOfIntegerRegisters() {
175202
pos = stack
176203
stack++
@@ -182,25 +209,54 @@ func callbackWrap(a *callbackArgs) {
182209
}
183210
args[i] = reflect.NewAt(fnType.In(i), unsafe.Pointer(&frame[pos])).Elem()
184211
}
185-
ret := fn.Call(args)
186-
if len(ret) > 0 {
187-
switch k := ret[0].Kind(); k {
188-
case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uintptr:
189-
a.result = uintptr(ret[0].Uint())
190-
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
191-
a.result = uintptr(ret[0].Int())
192-
case reflect.Bool:
193-
if ret[0].Bool() {
194-
a.result = 1
212+
}
213+
214+
// callbackWrapDarwinARM64 handles callbacks with C-packed stack argument layout on Darwin ARM64.
215+
func callbackWrapDarwinARM64(a *callbackArgs, fn reflect.Value, fnType reflect.Type, args []reflect.Value) {
216+
frameBytes := (*[callbackMaxFrame * 8]byte)(a.args)
217+
var floatsN int
218+
var intsN int
219+
// stackOffset tracks byte offset into the stack portion (after registers)
220+
stackOffset := (numOfIntegerRegisters() + numOfFloatRegisters) * 8
221+
222+
for i := range args {
223+
argType := fnType.In(i)
224+
argSize := int(argType.Size())
225+
// Natural alignment: align to the argument's size (min 1, max 8)
226+
alignment := argSize
227+
if alignment > 8 {
228+
alignment = 8
229+
}
230+
231+
switch argType.Kind() {
232+
case reflect.Float32, reflect.Float64:
233+
if floatsN >= numOfFloatRegisters {
234+
// Stack argument - align and use C packing
235+
stackOffset = (stackOffset + alignment - 1) & ^(alignment - 1)
236+
args[i] = reflect.NewAt(argType, unsafe.Pointer(&frameBytes[stackOffset])).Elem()
237+
stackOffset += argSize
195238
} else {
196-
a.result = 0
239+
// Register argument
240+
pos := floatsN * 8
241+
args[i] = reflect.NewAt(argType, unsafe.Pointer(&frameBytes[pos])).Elem()
197242
}
198-
case reflect.Pointer:
199-
a.result = ret[0].Pointer()
200-
case reflect.UnsafePointer:
201-
a.result = ret[0].Pointer()
243+
floatsN++
244+
case reflect.Struct:
245+
// This is the CDecl field
246+
args[i] = reflect.Zero(argType)
247+
continue
202248
default:
203-
panic("purego: unsupported kind: " + k.String())
249+
if intsN >= numOfIntegerRegisters() {
250+
// Stack argument - align and use C packing
251+
stackOffset = (stackOffset + alignment - 1) & ^(alignment - 1)
252+
args[i] = reflect.NewAt(argType, unsafe.Pointer(&frameBytes[stackOffset])).Elem()
253+
stackOffset += argSize
254+
} else {
255+
// Register argument - integers start after floats
256+
pos := (numOfFloatRegisters + intsN) * 8
257+
args[i] = reflect.NewAt(argType, unsafe.Pointer(&frameBytes[pos])).Elem()
258+
}
259+
intsN++
204260
}
205261
}
206262
}

syscall_windows.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ func NewCallback(fn any) uintptr {
4141
return syscall.NewCallback(fn)
4242
}
4343

44-
func isCallbackFunction(cfn uintptr) bool {
45-
return false
46-
}
47-
4844
func loadSymbol(handle uintptr, name string) (uintptr, error) {
4945
return syscall.GetProcAddress(syscall.Handle(handle), name)
5046
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+
#include <stdint.h>
5+
6+
typedef int32_t (*callback_int32)(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t,
7+
int32_t, int32_t, int32_t, int32_t);
8+
9+
int32_t callCallbackInt32Packing(const void *fp) {
10+
return ((callback_int32)(fp))(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37);
11+
}
12+
13+
typedef void (*callback_mixed)(int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int64_t,
14+
int32_t, int64_t, int32_t);
15+
16+
void callCallbackMixedPacking(const void *fp) {
17+
((callback_mixed)(fp))(1, 2, 3, 4, 5, 6, 7, 8, 100, 200, 300);
18+
}
19+
20+
typedef void (*callback_small_types)(int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int64_t,
21+
_Bool, int8_t, uint8_t, int16_t, uint16_t, int32_t);
22+
23+
void callCallbackSmallTypes(const void *fp) {
24+
((callback_small_types)(fp))(1, 2, 3, 4, 5, 6, 7, 8, 1, -42, 200, -1000, 50000, 123456);
25+
}

0 commit comments

Comments
 (0)