purego: only expand the last []any argument (#511)
Pass non-final []any arguments as slice pointers instead of panicking. Preserve expansion of final []any and ...any parameters as documented. Clarify the argument conversion rules and add regression tests for final argument expansion and non-final slice arguments. Closes #506
- commit
- 72b0dcd291d424a09c2a56411ac60f488bacbe7d
- author
- Hiroki KUMAZAKI · · 2026-09-06 16:32 UTC
- committer
- GitHub · · 2026-09-06 16:32 UTC
- parent
- 763bb99
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
2 files · +59 -8
M callback_test.go +46 -0
| @@ -411,3 +411,49 @@ func TestCallbackFloat32StackPacking(t *testing.T) { | |||
| 411 | 411 | t.Errorf("callCallback12Float32() = %d, want %d", got, want) | |
| 412 | 412 | } | |
| 413 | 413 | } | |
| 414 | + | ||
| 415 | + | func TestRegisterFuncExpandFinalAnyArgument(t *testing.T) { | |
| 416 | + | // The last argument is expanded into the C arguments held in it, | |
| 417 | + | // whether it is declared as ...any or as []any (#506). | |
| 418 | + | cb := purego.NewCallback(func(a, b, c uintptr) uintptr { | |
| 419 | + | return a*100 + b*10 + c | |
| 420 | + | }) | |
| 421 | + | const want = uintptr(123) | |
| 422 | + | t.Run("Variadic", func(t *testing.T) { | |
| 423 | + | var fn func(a uintptr, args ...any) uintptr | |
| 424 | + | purego.RegisterFunc(&fn, cb) | |
| 425 | + | if got := fn(1, uintptr(2), uintptr(3)); got != want { | |
| 426 | + | t.Errorf("func(uintptr, ...any) = %d, want %d", got, want) | |
| 427 | + | } | |
| 428 | + | }) | |
| 429 | + | t.Run("Slice", func(t *testing.T) { | |
| 430 | + | var fn func(a uintptr, args []any) uintptr | |
| 431 | + | purego.RegisterFunc(&fn, cb) | |
| 432 | + | if got := fn(1, []any{uintptr(2), uintptr(3)}); got != want { | |
| 433 | + | t.Errorf("func(uintptr, []any) = %d, want %d", got, want) | |
| 434 | + | } | |
| 435 | + | }) | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | func TestRegisterFuncKeepNonFinalAnyArgument(t *testing.T) { | |
| 439 | + | // A []any that isn't the last argument is a regular argument: | |
| 440 | + | // it is passed as one value and must not panic (#506). | |
| 441 | + | var first any | |
| 442 | + | var second uintptr | |
| 443 | + | cb := purego.NewCallback(func(args *any, b uintptr) uintptr { | |
| 444 | + | first = *args | |
| 445 | + | second = b | |
| 446 | + | return 0 | |
| 447 | + | }) | |
| 448 | + | var fn func(args []any, b uintptr) uintptr | |
| 449 | + | purego.RegisterFunc(&fn, cb) | |
| 450 | + | s := []any{uintptr(2), uintptr(3)} | |
| 451 | + | fn(s, 7) | |
| 452 | + | runtime.KeepAlive(s) | |
| 453 | + | if want := any(uintptr(2)); first != want { | |
| 454 | + | t.Errorf("the first C argument = %v, want %v", first, want) | |
| 455 | + | } | |
| 456 | + | if second != 7 { | |
| 457 | + | t.Errorf("the second C argument = %d, want 7", second) | |
| 458 | + | } | |
| 459 | + | } | |
| @@ -411,3 +411,49 @@ func TestCallbackFloat32StackPacking(t *testing.T) { | |||||
| 411 | t.Errorf("callCallback12Float32() = %d, want %d", got, want) | 411 | t.Errorf("callCallback12Float32() = %d, want %d", got, want) | ||
| 412 | } | 412 | } | ||
| 413 | } | 413 | } | ||
| 414 | + | ||||
| 415 | + | func TestRegisterFuncExpandFinalAnyArgument(t *testing.T) { | |||
| 416 | + | // The last argument is expanded into the C arguments held in it, | |||
| 417 | + | // whether it is declared as ...any or as []any (#506). | |||
| 418 | + | cb := purego.NewCallback(func(a, b, c uintptr) uintptr { | |||
| 419 | + | return a*100 + b*10 + c | |||
| 420 | + | }) | |||
| 421 | + | const want = uintptr(123) | |||
| 422 | + | t.Run("Variadic", func(t *testing.T) { | |||
| 423 | + | var fn func(a uintptr, args ...any) uintptr | |||
| 424 | + | purego.RegisterFunc(&fn, cb) | |||
| 425 | + | if got := fn(1, uintptr(2), uintptr(3)); got != want { | |||
| 426 | + | t.Errorf("func(uintptr, ...any) = %d, want %d", got, want) | |||
| 427 | + | } | |||
| 428 | + | }) | |||
| 429 | + | t.Run("Slice", func(t *testing.T) { | |||
| 430 | + | var fn func(a uintptr, args []any) uintptr | |||
| 431 | + | purego.RegisterFunc(&fn, cb) | |||
| 432 | + | if got := fn(1, []any{uintptr(2), uintptr(3)}); got != want { | |||
| 433 | + | t.Errorf("func(uintptr, []any) = %d, want %d", got, want) | |||
| 434 | + | } | |||
| 435 | + | }) | |||
| 436 | + | } | |||
| 437 | + | ||||
| 438 | + | func TestRegisterFuncKeepNonFinalAnyArgument(t *testing.T) { | |||
| 439 | + | // A []any that isn't the last argument is a regular argument: | |||
| 440 | + | // it is passed as one value and must not panic (#506). | |||
| 441 | + | var first any | |||
| 442 | + | var second uintptr | |||
| 443 | + | cb := purego.NewCallback(func(args *any, b uintptr) uintptr { | |||
| 444 | + | first = *args | |||
| 445 | + | second = b | |||
| 446 | + | return 0 | |||
| 447 | + | }) | |||
| 448 | + | var fn func(args []any, b uintptr) uintptr | |||
| 449 | + | purego.RegisterFunc(&fn, cb) | |||
| 450 | + | s := []any{uintptr(2), uintptr(3)} | |||
| 451 | + | fn(s, 7) | |||
| 452 | + | runtime.KeepAlive(s) | |||
| 453 | + | if want := any(uintptr(2)); first != want { | |||
| 454 | + | t.Errorf("the first C argument = %v, want %v", first, want) | |||
| 455 | + | } | |||
| 456 | + | if second != 7 { | |||
| 457 | + | t.Errorf("the second C argument = %d, want 7", second) | |||
| 458 | + | } | |||
| 459 | + | } | |||
M func.go +13 -8
| @@ -73,10 +73,12 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { | |||
| 73 | 73 | // unsafe.Pointer, *T <=> void* | |
| 74 | 74 | // []T => void* | |
| 75 | 75 | // | |
| 76 | - | // There is a special case when the last argument of fptr is a variadic interface (or []interface} | |
| 76 | + | // There is a special case when the last argument of fptr is a variadic interface (or []any): | |
| 77 | 77 | // it will be expanded into a call to the C function as if it had the arguments in that slice. | |
| 78 | 78 | // This means that using arg ...any is like a cast to the function with the arguments inside arg. | |
| 79 | 79 | // This is not the same as C variadic. | |
| 80 | + | // Only the last argument is expanded this way. A []any in any other position is an ordinary | |
| 81 | + | // argument and is passed as a single value like any other slice. | |
| 80 | 82 | // | |
| 81 | 83 | // # Memory | |
| 82 | 84 | // | |
| @@ -342,14 +344,17 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||
| 342 | 344 | } | |
| 343 | 345 | } | |
| 344 | 346 | for i, v := range args { | |
| 345 | - | if variadic, ok := reflect.TypeAssert[[]any](args[i]); ok { | |
| 346 | - | if i != len(args)-1 { | |
| 347 | - | panic("purego: can only expand last parameter") | |
| 348 | - | } | |
| 349 | - | for _, x := range variadic { | |
| 350 | - | keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 347 | + | // Only the last argument is expanded into several C arguments. | |
| 348 | + | // Both a declared ...any parameter and a final []any parameter | |
| 349 | + | // arrive here as a []any holding those arguments. A []any in any | |
| 350 | + | // other position is an ordinary argument that is passed as one value. | |
| 351 | + | if i == len(args)-1 { | |
| 352 | + | if variadic, ok := reflect.TypeAssert[[]any](args[i]); ok { | |
| 353 | + | for _, x := range variadic { | |
| 354 | + | keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 355 | + | } | |
| 356 | + | continue | |
| 351 | 357 | } | |
| 352 | - | continue | |
| 353 | 358 | } | |
| 354 | 359 | // Check if we need to start Darwin ARM64 C-style stack packing | |
| 355 | 360 | if runtime.GOARCH == "arm64" && isDarwin && shouldBundleStackArgs(v, numInts, numFloats) { | |
| @@ -73,10 +73,12 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { | |||||
| 73 | // unsafe.Pointer, *T <=> void* | 73 | // unsafe.Pointer, *T <=> void* | ||
| 74 | // []T => void* | 74 | // []T => void* | ||
| 75 | // | 75 | // | ||
| 76 | - | // There is a special case when the last argument of fptr is a variadic interface (or []interface} | 76 | + | // There is a special case when the last argument of fptr is a variadic interface (or []any): |
| 77 | // it will be expanded into a call to the C function as if it had the arguments in that slice. | 77 | // it will be expanded into a call to the C function as if it had the arguments in that slice. | ||
| 78 | // This means that using arg ...any is like a cast to the function with the arguments inside arg. | 78 | // This means that using arg ...any is like a cast to the function with the arguments inside arg. | ||
| 79 | // This is not the same as C variadic. | 79 | // This is not the same as C variadic. | ||
| 80 | + | // Only the last argument is expanded this way. A []any in any other position is an ordinary | |||
| 81 | + | // argument and is passed as a single value like any other slice. | |||
| 80 | // | 82 | // | ||
| 81 | // # Memory | 83 | // # Memory | ||
| 82 | // | 84 | // | ||
| @@ -342,14 +344,17 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||||
| 342 | } | 344 | } | ||
| 343 | } | 345 | } | ||
| 344 | for i, v := range args { | 346 | for i, v := range args { | ||
| 345 | - | if variadic, ok := reflect.TypeAssert[[]any](args[i]); ok { | 347 | + | // Only the last argument is expanded into several C arguments. |
| 346 | - | if i != len(args)-1 { | 348 | + | // Both a declared ...any parameter and a final []any parameter |
| 347 | - | panic("purego: can only expand last parameter") | 349 | + | // arrive here as a []any holding those arguments. A []any in any |
| 348 | - | } | 350 | + | // other position is an ordinary argument that is passed as one value. |
| 349 | - | for _, x := range variadic { | 351 | + | if i == len(args)-1 { |
| 350 | - | keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | 352 | + | if variadic, ok := reflect.TypeAssert[[]any](args[i]); ok { |
| 353 | + | for _, x := range variadic { | |||
| 354 | + | keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |||
| 355 | + | } | |||
| 356 | + | continue | |||
| 351 | } | 357 | } | ||
| 352 | - | continue | |||
| 353 | } | 358 | } | ||
| 354 | // Check if we need to start Darwin ARM64 C-style stack packing | 359 | // Check if we need to start Darwin ARM64 C-style stack packing | ||
| 355 | if runtime.GOARCH == "arm64" && isDarwin && shouldBundleStackArgs(v, numInts, numFloats) { | 360 | if runtime.GOARCH == "arm64" && isDarwin && shouldBundleStackArgs(v, numInts, numFloats) { | ||