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 Unverified
B5690EEEBB952194The key is unknown
6 files · +357 -103
M callback_test.go +233 -0
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "fmt" | |
| 10 | 10 | "os" | |
| 11 | 11 | "path/filepath" | |
| 12 | + | "runtime" | |
| 12 | 13 | "testing" | |
| 13 | 14 | "unsafe" | |
| 14 | 15 | ||
| @@ -173,3 +174,235 @@ func ExampleNewCallback_cdecl() { | |||
| 173 | 174 | ||
| 174 | 175 | // 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 | + | } | |
| @@ -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: 83 | 175 | // 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) { | |||
| 210 | 210 | } | |
| 211 | 211 | ||
| 212 | 212 | 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 | |
| 214 | 215 | if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { | |
| 215 | 216 | stackBytes := estimateStackBytes(ty) | |
| 216 | 217 | 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 | 228 | v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | |
| 233 | 229 | var sysargs [maxArgs]uintptr | |
| 234 | 230 | var floats [numOfFloatRegisters]uintptr | |
| @@ -305,9 +301,7 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||
| 305 | 301 | continue | |
| 306 | 302 | } | |
| 307 | 303 | // 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) { | |
| 311 | 305 | // Collect and separate remaining args into register vs stack | |
| 312 | 306 | stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats, | |
| 313 | 307 | keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| @@ -504,8 +498,8 @@ func numOfIntegerRegisters() int { | |||
| 504 | 498 | // estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation. | |
| 505 | 499 | // This is a conservative estimate used only for early error detection. | |
| 506 | 500 | func estimateStackBytes(ty reflect.Type) int { | |
| 507 | - | numInts, numFloats := 0, 0 | |
| 508 | - | stackBytes := 0 | |
| 501 | + | var numInts, numFloats int | |
| 502 | + | var stackBytes int | |
| 509 | 503 | ||
| 510 | 504 | for i := 0; i < ty.NumIn(); i++ { | |
| 511 | 505 | arg := ty.In(i) | |
| @@ -528,35 +522,3 @@ func estimateStackBytes(ty reflect.Type) int { | |||
| 528 | 522 | } | |
| 529 | 523 | 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 | - | } | |
| @@ -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 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 | |||
| 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 * 8 | 217 | 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]uintptr | 229 | var sysargs [maxArgs]uintptr | ||
| 234 | var floats [numOfFloatRegisters]uintptr | 230 | var floats [numOfFloatRegisters]uintptr | ||
| @@ -305,9 +301,7 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||||
| 305 | continue | 301 | continue | ||
| 306 | } | 302 | } | ||
| 307 | // Check if we need to start Darwin ARM64 C-style stack packing | 303 | // 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 | 304 | + | 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 stack | 305 | // 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, 0 | 501 | + | var numInts, numFloats int |
| 508 | - | stackBytes := 0 | 502 | + | 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 stackBytes | 523 | 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) { | |||
| 147 | 147 | frame := (*[callbackMaxFrame]uintptr)(a.args) | |
| 148 | 148 | var floatsN int // floatsN represents the number of float arguments processed | |
| 149 | 149 | 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 | 151 | // 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) | |
| 153 | 156 | for i := range args { | |
| 154 | - | var pos int | |
| 155 | - | switch fnType.In(i).Kind() { | |
| 157 | + | inType := fnType.In(i) | |
| 158 | + | switch inType.Kind() { | |
| 156 | 159 | case reflect.Float32, reflect.Float64: | |
| 157 | 160 | 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 | + | } | |
| 160 | 168 | } else { | |
| 161 | - | pos = floatsN | |
| 169 | + | args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem() | |
| 162 | 170 | } | |
| 163 | 171 | floatsN++ | |
| 164 | 172 | case reflect.Struct: | |
| 165 | 173 | // This is the CDecl field | |
| 166 | - | args[i] = reflect.Zero(fnType.In(i)) | |
| 167 | - | continue | |
| 174 | + | args[i] = reflect.Zero(inType) | |
| 168 | 175 | default: | |
| 169 | - | ||
| 170 | 176 | 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 | + | } | |
| 173 | 184 | } else { | |
| 174 | 185 | // 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() | |
| 176 | 188 | } | |
| 177 | 189 | intsN++ | |
| 178 | 190 | } | |
| 179 | - | args[i] = reflect.NewAt(fnType.In(i), unsafe.Pointer(&frame[pos])).Elem() | |
| 180 | 191 | } | |
| 181 | 192 | ret := fn.Call(args) | |
| 182 | 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 | 238 | // callbackasmAddr returns address of runtime.callbackasm | |
| 205 | 239 | // function adjusted by i. | |
| 206 | 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 | 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 | - | } | |
| @@ -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 processed | 148 | var floatsN int // floatsN represents the number of float arguments processed | ||
| 149 | var intsN int // intsN represents the number of integer arguments processed | 149 | 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() + 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) | |||
| 153 | for i := range args { | 156 | for i := range args { | ||
| 154 | - | var pos int | 157 | + | 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 = stack | 161 | + | 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 = floatsN | 169 | + | 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 field | 173 | // 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 = stack | 177 | + | 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 frame | 185 | // 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() | |||
| 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.callbackasm | 238 | // 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 { | |||
| 44 | 44 | func loadSymbol(handle uintptr, name string) (uintptr, error) { | |
| 45 | 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 | - | } | |
| @@ -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 | + | } | |||