detached 72b0dcd

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 UnverifiedB5690EEEBB952194 The key is unknown
2 files · +59 -8
M callback_test.go +46 -0
@@ -411,3 +411,49 @@ func TestCallbackFloat32StackPacking(t *testing.T) {
411411 t.Errorf("callCallback12Float32() = %d, want %d", got, want)
412412 }
413413 }
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) {
7373 // unsafe.Pointer, *T <=> void*
7474 // []T => void*
7575 //
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):
7777 // it will be expanded into a call to the C function as if it had the arguments in that slice.
7878 // This means that using arg ...any is like a cast to the function with the arguments inside arg.
7979 // 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.
8082 //
8183 // # Memory
8284 //
@@ -342,14 +344,17 @@ func RegisterFunc(fptr any, cfn uintptr) {
342344 }
343345 }
344346 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
351357 }
352- continue
353358 }
354359 // Check if we need to start Darwin ARM64 C-style stack packing
355360 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 // # Memory83 // # 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 packing359 // 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) {

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