detached d2ade5c

feat: FFI optimizations for high-frequency callback/function dispatch

- Fix []string argument marshalling (addValue: []string → NULL-terminated char**)
- Fix []string return value handling (char** → []string)
- Replace callback mutex with RWMutex (append-only array, reads safe without exclusive lock)
- Pre-allocate callback argument buffers via sync.Pool (eliminate per-call make)
- Fast path for pointer-only RegisterFunc calls (skip keepAlive/defer overhead)
- Add SyscallSelf helper for zero-alloc method-style calls
- Eliminate return value heap allocation for scalar types (direct reflect.ValueOf)
- Support reflect.Array arguments in addValue
- Add structs.HostLayout to syscall15Args and callbackArgs (ABI safety)
commit
d2ade5cff9dd439a48d943f7ae6c015d8e79797b
author
brice · · 2026-03-23 09:46 UTC
parent
386f7c8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +397 -107
M func.go +230 -99
@@ -160,7 +160,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
160160 }
161161 case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
162162 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer,
163- reflect.Slice, reflect.Bool:
163+ reflect.Slice, reflect.Array, reflect.Bool:
164164 if ints < numOfIntegerRegisters() {
165165 ints++
166166 } else {
@@ -218,6 +218,28 @@ func RegisterFunc(fptr any, cfn uintptr) {
218218 }
219219 }
220220
221+ // Task 3: Analyze types at RegisterFunc time to determine if keepAlive is needed.
222+ // keepAlive is only used for: string args (CString alloc), struct args (addStruct),
223+ // struct returns > maxRegAllocStructSize (hidden pointer), array args (addressable copy),
224+ // and variadic []any args (unknown types at registration time).
225+ needsKeepAlive := false
226+ for i := 0; i < ty.NumIn(); i++ {
227+ switch ty.In(i).Kind() {
228+ case reflect.String, reflect.Struct, reflect.Array:
229+ needsKeepAlive = true
230+ case reflect.Interface:
231+ // variadic []any — could contain anything
232+ needsKeepAlive = true
233+ }
234+ }
235+ if ty.NumOut() == 1 {
236+ switch ty.Out(0).Kind() {
237+ case reflect.Struct:
238+ // struct returns may need hidden pointer allocation stored in keepAlive
239+ needsKeepAlive = true
240+ }
241+ }
242+
221243 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
222244 var sysargs [maxArgs]uintptr
223245 // Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free,
@@ -264,132 +286,212 @@ func RegisterFunc(fptr any, cfn uintptr) {
264286 addFloat = addStack
265287 }
266288
267- var keepAlive []any
268- defer func() {
269- runtime.KeepAlive(keepAlive)
270- runtime.KeepAlive(args)
271- }()
289+ if needsKeepAlive {
290+ // Slow path: allocate keepAlive slice and defer cleanup.
291+ // Used when args include strings, structs, arrays, or variadic []any.
292+ var keepAlive []any
293+ defer func() {
294+ runtime.KeepAlive(keepAlive)
295+ runtime.KeepAlive(args)
296+ }()
272297
273- var arm64_r8 uintptr
274- if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
275- outType := ty.Out(0)
276- if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
277- val := reflect.New(outType)
278- keepAlive = append(keepAlive, val)
279- addInt(val.Pointer())
280- } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
281- isAllFloats, numFields := isAllSameFloat(outType)
282- if !isAllFloats || numFields > 4 {
298+ var arm64_r8 uintptr
299+ if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
300+ outType := ty.Out(0)
301+ if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
283302 val := reflect.New(outType)
284303 keepAlive = append(keepAlive, val)
285- arm64_r8 = val.Pointer()
304+ addInt(val.Pointer())
305+ } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
306+ isAllFloats, numFields := isAllSameFloat(outType)
307+ if !isAllFloats || numFields > 4 {
308+ val := reflect.New(outType)
309+ keepAlive = append(keepAlive, val)
310+ arm64_r8 = val.Pointer()
311+ }
312+ }
313+ }
314+ for i, v := range args {
315+ if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok {
316+ if i != len(args)-1 {
317+ panic("purego: can only expand last parameter")
318+ }
319+ for _, x := range variadic {
320+ keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
321+ }
322+ continue
323+ }
324+ // Check if we need to start Darwin ARM64 C-style stack packing
325+ if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
326+ // Collect and separate remaining args into register vs stack
327+ stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
328+ keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
329+ keepAlive = newKeepAlive
330+
331+ // Bundle stack arguments with C-style packing
332+ bundleStackArgs(stackArgs, addStack)
333+ break
286334 }
335+ keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
287336 }
337+ return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, arm64_r8, args)
288338 }
339+
340+ // Fast path: no keepAlive needed (pointer/integer/bool/float args only).
341+ // Skip keepAlive slice allocation and defer overhead.
342+ defer runtime.KeepAlive(args)
343+
289344 for i, v := range args {
290345 if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok {
291346 if i != len(args)-1 {
292347 panic("purego: can only expand last parameter")
293348 }
294349 for _, x := range variadic {
295- keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
350+ addValue(reflect.ValueOf(x), nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
296351 }
297352 continue
298353 }
299354 // Check if we need to start Darwin ARM64 C-style stack packing
300355 if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
301- // Collect and separate remaining args into register vs stack
302- stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
303- keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
304- keepAlive = newKeepAlive
305-
306- // Bundle stack arguments with C-style packing
356+ stackArgs, _ := collectStackArgs(args, i, numInts, numFloats,
357+ nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
307358 bundleStackArgs(stackArgs, addStack)
308359 break
309360 }
310- keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
361+ addValue(v, nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
311362 }
312363
313- syscall := thePool.Get().(*syscall15Args)
314- defer thePool.Put(syscall)
364+ return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, 0, args)
365+ })
366+ fn.Set(v)
367+}
368+
369+// callAndReturn performs the actual C function call and constructs the return value.
370+// It is shared between the fast path (no keepAlive) and slow path (with keepAlive).
371+func callAndReturn(ty reflect.Type, is32bit bool, cfn uintptr, sysargs *[maxArgs]uintptr, floats *[maxArgs]uintptr, arm64_r8 uintptr, args []reflect.Value) []reflect.Value {
372+ syscall := thePool.Get().(*syscall15Args)
373+ defer thePool.Put(syscall)
315374
316- if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
317- syscall.Set(cfn, sysargs[:], floats[:], 0)
318- runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
319- } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
320- // Use the normal arm64 calling convention even on Windows
321- syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
322- runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
323- } else {
324- *syscall = syscall15Args{}
325- // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats
326- syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4],
327- sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
328- sysargs[12], sysargs[13], sysargs[14])
329- syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
375+ if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
376+ syscall.Set(cfn, sysargs[:], floats[:], 0)
377+ runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
378+ } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
379+ // Use the normal arm64 calling convention even on Windows
380+ syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
381+ runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
382+ } else {
383+ *syscall = syscall15Args{}
384+ // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats
385+ syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4],
386+ sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
387+ sysargs[12], sysargs[13], sysargs[14])
388+ syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
389+ }
390+ if ty.NumOut() == 0 {
391+ return nil
392+ }
393+ outType := ty.Out(0)
394+ // Task 5: For simple scalar return types, construct the reflect.Value directly
395+ // instead of allocating via reflect.New(outType).Elem().
396+ // reflect.ValueOf for scalar types avoids the heap allocation that reflect.New incurs.
397+ var v reflect.Value
398+ switch outType.Kind() {
399+ case reflect.Uintptr:
400+ v = reflect.ValueOf(uintptr(syscall.a1))
401+ case reflect.Uint:
402+ v = reflect.ValueOf(uint(syscall.a1))
403+ case reflect.Uint8:
404+ v = reflect.ValueOf(uint8(syscall.a1))
405+ case reflect.Uint16:
406+ v = reflect.ValueOf(uint16(syscall.a1))
407+ case reflect.Uint32:
408+ v = reflect.ValueOf(uint32(syscall.a1))
409+ case reflect.Uint64:
410+ v = reflect.ValueOf(uint64(syscall.a1))
411+ case reflect.Int:
412+ v = reflect.ValueOf(int(syscall.a1))
413+ case reflect.Int8:
414+ v = reflect.ValueOf(int8(syscall.a1))
415+ case reflect.Int16:
416+ v = reflect.ValueOf(int16(syscall.a1))
417+ case reflect.Int32:
418+ v = reflect.ValueOf(int32(syscall.a1))
419+ case reflect.Int64:
420+ v = reflect.ValueOf(int64(syscall.a1))
421+ case reflect.Bool:
422+ v = reflect.ValueOf(byte(syscall.a1) != 0)
423+ case reflect.UnsafePointer:
424+ // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
425+ v = reflect.New(outType).Elem()
426+ v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
427+ case reflect.Pointer:
428+ v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem()
429+ case reflect.Func:
430+ // wrap this C function in a nicely typed Go function
431+ v = reflect.New(outType)
432+ RegisterFunc(v.Interface(), syscall.a1)
433+ case reflect.String:
434+ v = reflect.ValueOf(strings.GoString(syscall.a1))
435+ case reflect.Float32:
436+ // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
437+ // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
438+ // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
439+ // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
440+ // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
441+ v = reflect.New(outType).Elem()
442+ switch runtime.GOARCH {
443+ case "386":
444+ v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
445+ case "ppc64le":
446+ v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
447+ case "s390x":
448+ // S390X is big-endian: float32 in upper 32 bits of 64-bit register
449+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
450+ default:
451+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
330452 }
331- if ty.NumOut() == 0 {
332- return nil
453+ case reflect.Float64:
454+ // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
455+ // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
456+ v = reflect.New(outType).Elem()
457+ if is32bit {
458+ v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
459+ } else {
460+ v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
333461 }
334- outType := ty.Out(0)
335- v := reflect.New(outType).Elem()
336- switch outType.Kind() {
337- case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
338- v.SetUint(uint64(syscall.a1))
339- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
340- v.SetInt(int64(syscall.a1))
341- case reflect.Bool:
342- v.SetBool(byte(syscall.a1) != 0)
343- case reflect.UnsafePointer:
344- // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
345- v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
346- case reflect.Pointer:
347- v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem()
348- case reflect.Func:
349- // wrap this C function in a nicely typed Go function
350- v = reflect.New(outType)
351- RegisterFunc(v.Interface(), syscall.a1)
352- case reflect.String:
353- v.SetString(strings.GoString(syscall.a1))
354- case reflect.Float32:
355- // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
356- // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
357- // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
358- // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
359- // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
360- switch runtime.GOARCH {
361- case "386":
362- v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
363- case "ppc64le":
364- v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
365- case "s390x":
366- // S390X is big-endian: float32 in upper 32 bits of 64-bit register
367- v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
368- default:
369- v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
370- }
371- case reflect.Float64:
372- // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
373- // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
374- if is32bit {
375- v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
462+ case reflect.Struct:
463+ v = getStruct(outType, *syscall)
464+ case reflect.Slice:
465+ if outType.Elem().Kind() == reflect.String {
466+ // C returns char** (NULL-terminated array of strings)
467+ if syscall.a1 == 0 {
468+ v = reflect.Zero(outType)
376469 } else {
377- v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
470+ var result []string
471+ ptr := syscall.a1
472+ for {
473+ strPtr := *(*uintptr)(unsafe.Pointer(ptr))
474+ if strPtr == 0 {
475+ break
476+ }
477+ result = append(result, strings.GoString(strPtr))
478+ ptr += unsafe.Sizeof(uintptr(0))
479+ }
480+ v = reflect.ValueOf(result)
378481 }
379- case reflect.Struct:
380- v = getStruct(outType, *syscall)
381- default:
382- panic("purego: unsupported return kind: " + outType.Kind().String())
383- }
384- if len(args) > 0 {
385- // reuse args slice instead of allocating one when possible
386- args[0] = v
387- return args[:1]
388482 } else {
389- return []reflect.Value{v}
483+ panic("purego: unsupported return slice element kind: " + outType.Elem().Kind().String())
390484 }
391- })
392- fn.Set(v)
485+ default:
486+ panic("purego: unsupported return kind: " + outType.Kind().String())
487+ }
488+ if len(args) > 0 {
489+ // reuse args slice instead of allocating one when possible
490+ args[0] = v
491+ return args[:1]
492+ } else {
493+ return []reflect.Value{v}
494+ }
393495 }
394496
395497 func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {
@@ -403,9 +505,27 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
403505 addInt(uintptr(v.Uint()))
404506 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
405507 addInt(uintptr(v.Int()))
406- case reflect.Pointer, reflect.UnsafePointer, reflect.Slice:
508+ case reflect.Pointer, reflect.UnsafePointer:
407509 // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
408510 addInt(v.Pointer())
511+ case reflect.Slice:
512+ if v.Type().Elem().Kind() == reflect.String {
513+ ss := v.Interface().([]string)
514+ if ss == nil {
515+ addInt(0)
516+ } else {
517+ res := make([]*byte, len(ss)+1)
518+ for i, s := range ss {
519+ res[i] = strings.CString(s)
520+ }
521+ // NULL-terminated for C
522+ res[len(ss)] = nil
523+ keepAlive = append(keepAlive, res)
524+ addInt(uintptr(unsafe.Pointer(&res[0])))
525+ }
526+ } else {
527+ addInt(v.Pointer())
528+ }
409529 case reflect.Func:
410530 addInt(NewCallback(v.Interface()))
411531 case reflect.Bool:
@@ -429,6 +549,17 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
429549 } else {
430550 addFloat(uintptr(math.Float64bits(v.Float())))
431551 }
552+ case reflect.Array:
553+ // Task 7: Support fixed-size array arguments by passing a pointer to the array data.
554+ if v.CanAddr() {
555+ addInt(v.Addr().Pointer())
556+ } else {
557+ // Array is not addressable (e.g. passed by value); make an addressable copy.
558+ tmp := reflect.New(v.Type()).Elem()
559+ tmp.Set(v)
560+ keepAlive = append(keepAlive, tmp)
561+ addInt(tmp.Addr().Pointer())
562+ }
432563 case reflect.Struct:
433564 keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
434565 default:
@@ -160,7 +160,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
160 }160 }
161 case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,161 case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
162 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer,162 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer,
163- reflect.Slice, reflect.Bool:163+ reflect.Slice, reflect.Array, reflect.Bool:
164 if ints < numOfIntegerRegisters() {164 if ints < numOfIntegerRegisters() {
165 ints++165 ints++
166 } else {166 } else {
@@ -218,6 +218,28 @@ func RegisterFunc(fptr any, cfn uintptr) {
218 }218 }
219 }219 }
220 220
221+ // Task 3: Analyze types at RegisterFunc time to determine if keepAlive is needed.
222+ // keepAlive is only used for: string args (CString alloc), struct args (addStruct),
223+ // struct returns > maxRegAllocStructSize (hidden pointer), array args (addressable copy),
224+ // and variadic []any args (unknown types at registration time).
225+ needsKeepAlive := false
226+ for i := 0; i < ty.NumIn(); i++ {
227+ switch ty.In(i).Kind() {
228+ case reflect.String, reflect.Struct, reflect.Array:
229+ needsKeepAlive = true
230+ case reflect.Interface:
231+ // variadic []any — could contain anything
232+ needsKeepAlive = true
233+ }
234+ }
235+ if ty.NumOut() == 1 {
236+ switch ty.Out(0).Kind() {
237+ case reflect.Struct:
238+ // struct returns may need hidden pointer allocation stored in keepAlive
239+ needsKeepAlive = true
240+ }
241+ }
242+
221 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {243 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
222 var sysargs [maxArgs]uintptr244 var sysargs [maxArgs]uintptr
223 // Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free,245 // Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free,
@@ -264,132 +286,212 @@ func RegisterFunc(fptr any, cfn uintptr) {
264 addFloat = addStack286 addFloat = addStack
265 }287 }
266 288
267- var keepAlive []any289+ if needsKeepAlive {
268- defer func() {290+ // Slow path: allocate keepAlive slice and defer cleanup.
269- runtime.KeepAlive(keepAlive)291+ // Used when args include strings, structs, arrays, or variadic []any.
270- runtime.KeepAlive(args)292+ var keepAlive []any
271- }()293+ defer func() {
294+ runtime.KeepAlive(keepAlive)
295+ runtime.KeepAlive(args)
296+ }()
272 297
273- var arm64_r8 uintptr298+ var arm64_r8 uintptr
274- if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {299+ if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
275- outType := ty.Out(0)300+ outType := ty.Out(0)
276- if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {301+ if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
277- val := reflect.New(outType)
278- keepAlive = append(keepAlive, val)
279- addInt(val.Pointer())
280- } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
281- isAllFloats, numFields := isAllSameFloat(outType)
282- if !isAllFloats || numFields > 4 {
283 val := reflect.New(outType)302 val := reflect.New(outType)
284 keepAlive = append(keepAlive, val)303 keepAlive = append(keepAlive, val)
285- arm64_r8 = val.Pointer()304+ addInt(val.Pointer())
305+ } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
306+ isAllFloats, numFields := isAllSameFloat(outType)
307+ if !isAllFloats || numFields > 4 {
308+ val := reflect.New(outType)
309+ keepAlive = append(keepAlive, val)
310+ arm64_r8 = val.Pointer()
311+ }
312+ }
313+ }
314+ for i, v := range args {
315+ if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok {
316+ if i != len(args)-1 {
317+ panic("purego: can only expand last parameter")
318+ }
319+ for _, x := range variadic {
320+ keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
321+ }
322+ continue
323+ }
324+ // Check if we need to start Darwin ARM64 C-style stack packing
325+ if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
326+ // Collect and separate remaining args into register vs stack
327+ stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
328+ keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
329+ keepAlive = newKeepAlive
330+
331+ // Bundle stack arguments with C-style packing
332+ bundleStackArgs(stackArgs, addStack)
333+ break
286 }334 }
335+ keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
287 }336 }
337+ return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, arm64_r8, args)
288 }338 }
339+
340+ // Fast path: no keepAlive needed (pointer/integer/bool/float args only).
341+ // Skip keepAlive slice allocation and defer overhead.
342+ defer runtime.KeepAlive(args)
343+
289 for i, v := range args {344 for i, v := range args {
290 if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok {345 if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok {
291 if i != len(args)-1 {346 if i != len(args)-1 {
292 panic("purego: can only expand last parameter")347 panic("purego: can only expand last parameter")
293 }348 }
294 for _, x := range variadic {349 for _, x := range variadic {
295- keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)350+ addValue(reflect.ValueOf(x), nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
296 }351 }
297 continue352 continue
298 }353 }
299 // Check if we need to start Darwin ARM64 C-style stack packing354 // Check if we need to start Darwin ARM64 C-style stack packing
300 if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {355 if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
301- // Collect and separate remaining args into register vs stack356+ stackArgs, _ := collectStackArgs(args, i, numInts, numFloats,
302- stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,357+ nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
303- keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
304- keepAlive = newKeepAlive
305-
306- // Bundle stack arguments with C-style packing
307 bundleStackArgs(stackArgs, addStack)358 bundleStackArgs(stackArgs, addStack)
308 break359 break
309 }360 }
310- keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)361+ addValue(v, nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
311 }362 }
312 363
313- syscall := thePool.Get().(*syscall15Args)364+ return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, 0, args)
314- defer thePool.Put(syscall)365+ })
366+ fn.Set(v)
367+}
368+
369+// callAndReturn performs the actual C function call and constructs the return value.
370+// It is shared between the fast path (no keepAlive) and slow path (with keepAlive).
371+func callAndReturn(ty reflect.Type, is32bit bool, cfn uintptr, sysargs *[maxArgs]uintptr, floats *[maxArgs]uintptr, arm64_r8 uintptr, args []reflect.Value) []reflect.Value {
372+ syscall := thePool.Get().(*syscall15Args)
373+ defer thePool.Put(syscall)
315 374
316- if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {375+ if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
317- syscall.Set(cfn, sysargs[:], floats[:], 0)376+ syscall.Set(cfn, sysargs[:], floats[:], 0)
318- runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))377+ runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
319- } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {378+ } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
320- // Use the normal arm64 calling convention even on Windows379+ // Use the normal arm64 calling convention even on Windows
321- syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)380+ syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
322- runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))381+ runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
323- } else {382+ } else {
324- *syscall = syscall15Args{}383+ *syscall = syscall15Args{}
325- // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats384+ // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats
326- syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4],385+ syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4],
327- sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],386+ sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
328- sysargs[12], sysargs[13], sysargs[14])387+ sysargs[12], sysargs[13], sysargs[14])
329- syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support388+ syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
389+ }
390+ if ty.NumOut() == 0 {
391+ return nil
392+ }
393+ outType := ty.Out(0)
394+ // Task 5: For simple scalar return types, construct the reflect.Value directly
395+ // instead of allocating via reflect.New(outType).Elem().
396+ // reflect.ValueOf for scalar types avoids the heap allocation that reflect.New incurs.
397+ var v reflect.Value
398+ switch outType.Kind() {
399+ case reflect.Uintptr:
400+ v = reflect.ValueOf(uintptr(syscall.a1))
401+ case reflect.Uint:
402+ v = reflect.ValueOf(uint(syscall.a1))
403+ case reflect.Uint8:
404+ v = reflect.ValueOf(uint8(syscall.a1))
405+ case reflect.Uint16:
406+ v = reflect.ValueOf(uint16(syscall.a1))
407+ case reflect.Uint32:
408+ v = reflect.ValueOf(uint32(syscall.a1))
409+ case reflect.Uint64:
410+ v = reflect.ValueOf(uint64(syscall.a1))
411+ case reflect.Int:
412+ v = reflect.ValueOf(int(syscall.a1))
413+ case reflect.Int8:
414+ v = reflect.ValueOf(int8(syscall.a1))
415+ case reflect.Int16:
416+ v = reflect.ValueOf(int16(syscall.a1))
417+ case reflect.Int32:
418+ v = reflect.ValueOf(int32(syscall.a1))
419+ case reflect.Int64:
420+ v = reflect.ValueOf(int64(syscall.a1))
421+ case reflect.Bool:
422+ v = reflect.ValueOf(byte(syscall.a1) != 0)
423+ case reflect.UnsafePointer:
424+ // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
425+ v = reflect.New(outType).Elem()
426+ v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
427+ case reflect.Pointer:
428+ v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem()
429+ case reflect.Func:
430+ // wrap this C function in a nicely typed Go function
431+ v = reflect.New(outType)
432+ RegisterFunc(v.Interface(), syscall.a1)
433+ case reflect.String:
434+ v = reflect.ValueOf(strings.GoString(syscall.a1))
435+ case reflect.Float32:
436+ // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
437+ // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
438+ // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
439+ // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
440+ // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
441+ v = reflect.New(outType).Elem()
442+ switch runtime.GOARCH {
443+ case "386":
444+ v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
445+ case "ppc64le":
446+ v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
447+ case "s390x":
448+ // S390X is big-endian: float32 in upper 32 bits of 64-bit register
449+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
450+ default:
451+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
330 }452 }
331- if ty.NumOut() == 0 {453+ case reflect.Float64:
332- return nil454+ // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
455+ // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
456+ v = reflect.New(outType).Elem()
457+ if is32bit {
458+ v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
459+ } else {
460+ v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
333 }461 }
334- outType := ty.Out(0)462+ case reflect.Struct:
335- v := reflect.New(outType).Elem()463+ v = getStruct(outType, *syscall)
336- switch outType.Kind() {464+ case reflect.Slice:
337- case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:465+ if outType.Elem().Kind() == reflect.String {
338- v.SetUint(uint64(syscall.a1))466+ // C returns char** (NULL-terminated array of strings)
339- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:467+ if syscall.a1 == 0 {
340- v.SetInt(int64(syscall.a1))468+ v = reflect.Zero(outType)
341- case reflect.Bool:
342- v.SetBool(byte(syscall.a1) != 0)
343- case reflect.UnsafePointer:
344- // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
345- v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
346- case reflect.Pointer:
347- v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem()
348- case reflect.Func:
349- // wrap this C function in a nicely typed Go function
350- v = reflect.New(outType)
351- RegisterFunc(v.Interface(), syscall.a1)
352- case reflect.String:
353- v.SetString(strings.GoString(syscall.a1))
354- case reflect.Float32:
355- // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
356- // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
357- // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
358- // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
359- // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
360- switch runtime.GOARCH {
361- case "386":
362- v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
363- case "ppc64le":
364- v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
365- case "s390x":
366- // S390X is big-endian: float32 in upper 32 bits of 64-bit register
367- v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
368- default:
369- v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
370- }
371- case reflect.Float64:
372- // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
373- // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
374- if is32bit {
375- v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
376 } else {469 } else {
377- v.SetFloat(math.Float64frombits(uint64(syscall.f1)))470+ var result []string
471+ ptr := syscall.a1
472+ for {
473+ strPtr := *(*uintptr)(unsafe.Pointer(ptr))
474+ if strPtr == 0 {
475+ break
476+ }
477+ result = append(result, strings.GoString(strPtr))
478+ ptr += unsafe.Sizeof(uintptr(0))
479+ }
480+ v = reflect.ValueOf(result)
378 }481 }
379- case reflect.Struct:
380- v = getStruct(outType, *syscall)
381- default:
382- panic("purego: unsupported return kind: " + outType.Kind().String())
383- }
384- if len(args) > 0 {
385- // reuse args slice instead of allocating one when possible
386- args[0] = v
387- return args[:1]
388 } else {482 } else {
389- return []reflect.Value{v}483+ panic("purego: unsupported return slice element kind: " + outType.Elem().Kind().String())
390 }484 }
391- })485+ default:
392- fn.Set(v)486+ panic("purego: unsupported return kind: " + outType.Kind().String())
487+ }
488+ if len(args) > 0 {
489+ // reuse args slice instead of allocating one when possible
490+ args[0] = v
491+ return args[:1]
492+ } else {
493+ return []reflect.Value{v}
494+ }
393 }495 }
394 496
395 func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {497 func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {
@@ -403,9 +505,27 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
403 addInt(uintptr(v.Uint()))505 addInt(uintptr(v.Uint()))
404 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:506 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
405 addInt(uintptr(v.Int()))507 addInt(uintptr(v.Int()))
406- case reflect.Pointer, reflect.UnsafePointer, reflect.Slice:508+ case reflect.Pointer, reflect.UnsafePointer:
407 // There is no need to keepAlive this pointer separately because it is kept alive in the args variable509 // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
408 addInt(v.Pointer())510 addInt(v.Pointer())
511+ case reflect.Slice:
512+ if v.Type().Elem().Kind() == reflect.String {
513+ ss := v.Interface().([]string)
514+ if ss == nil {
515+ addInt(0)
516+ } else {
517+ res := make([]*byte, len(ss)+1)
518+ for i, s := range ss {
519+ res[i] = strings.CString(s)
520+ }
521+ // NULL-terminated for C
522+ res[len(ss)] = nil
523+ keepAlive = append(keepAlive, res)
524+ addInt(uintptr(unsafe.Pointer(&res[0])))
525+ }
526+ } else {
527+ addInt(v.Pointer())
528+ }
409 case reflect.Func:529 case reflect.Func:
410 addInt(NewCallback(v.Interface()))530 addInt(NewCallback(v.Interface()))
411 case reflect.Bool:531 case reflect.Bool:
@@ -429,6 +549,17 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
429 } else {549 } else {
430 addFloat(uintptr(math.Float64bits(v.Float())))550 addFloat(uintptr(math.Float64bits(v.Float())))
431 }551 }
552+ case reflect.Array:
553+ // Task 7: Support fixed-size array arguments by passing a pointer to the array data.
554+ if v.CanAddr() {
555+ addInt(v.Addr().Pointer())
556+ } else {
557+ // Array is not addressable (e.g. passed by value); make an addressable copy.
558+ tmp := reflect.New(v.Type()).Elem()
559+ tmp.Set(v)
560+ keepAlive = append(keepAlive, tmp)
561+ addInt(tmp.Addr().Pointer())
562+ }
432 case reflect.Struct:563 case reflect.Struct:
433 keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)564 keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
434 default:565 default:
A hostlayout_go123.go +12 -0
@@ -0,0 +1,12 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build go1.23
5+
6+package purego
7+
8+import "structs"
9+
10+// hostLayout is structs.HostLayout on Go 1.23+, ensuring the compiler
11+// uses host (C-compatible) memory layout for structs that embed it.
12+type hostLayout = structs.HostLayout
@@ -0,0 +1,12 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build go1.23
5+
6+package purego
7+
8+import "structs"
9+
10+// hostLayout is structs.HostLayout on Go 1.23+, ensuring the compiler
11+// uses host (C-compatible) memory layout for structs that embed it.
12+type hostLayout = structs.HostLayout
A hostlayout_nogo123.go +11 -0
@@ -0,0 +1,11 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !go1.23
5+
6+package purego
7+
8+// hostLayout is a zero-size placeholder for Go versions before 1.23
9+// that lack the structs package. On Go 1.23+ this is replaced by
10+// structs.HostLayout which tells the compiler to use host memory layout.
11+type hostLayout struct{}
@@ -0,0 +1,11 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !go1.23
5+
6+package purego
7+
8+// hostLayout is a zero-size placeholder for Go versions before 1.23
9+// that lack the structs package. On Go 1.23+ this is replaced by
10+// structs.HostLayout which tells the compiler to use host memory layout.
11+type hostLayout struct{}
M syscall.go +21 -0
@@ -17,6 +17,7 @@ const (
1717 )
1818
1919 type syscall15Args struct {
20+ _ hostLayout
2021 fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
2122 f1, f2, f3, f4, f5, f6, f7, f8 uintptr
2223 arm64_r8 uintptr
@@ -81,3 +82,23 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
8182 copy(tmp[:], args)
8283 return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
8384 }
85+
86+// SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
87+// It is designed for method-like calls where the first argument is always a receiver pointer.
88+// This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
89+//
90+//go:uintptrescapes
91+func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
92+ if fn == 0 {
93+ panic("purego: fn is nil")
94+ }
95+ if len(args) > maxArgs-1 {
96+ panic("purego: too many arguments to SyscallSelf")
97+ }
98+ // Use a stack-allocated array to prepend self without heap allocation.
99+ // self goes in tmp[0], remaining args follow.
100+ var tmp [maxArgs]uintptr
101+ tmp[0] = self
102+ copy(tmp[1:], args)
103+ return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
104+}
@@ -17,6 +17,7 @@ const (
17 )17 )
18 18
19 type syscall15Args struct {19 type syscall15Args struct {
20+ _ hostLayout
20 fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr21 fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
21 f1, f2, f3, f4, f5, f6, f7, f8 uintptr22 f1, f2, f3, f4, f5, f6, f7, f8 uintptr
22 arm64_r8 uintptr23 arm64_r8 uintptr
@@ -81,3 +82,23 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
81 copy(tmp[:], args)82 copy(tmp[:], args)
82 return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])83 return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
83 }84 }
85+
86+// SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
87+// It is designed for method-like calls where the first argument is always a receiver pointer.
88+// This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
89+//
90+//go:uintptrescapes
91+func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
92+ if fn == 0 {
93+ panic("purego: fn is nil")
94+ }
95+ if len(args) > maxArgs-1 {
96+ panic("purego: too many arguments to SyscallSelf")
97+ }
98+ // Use a stack-allocated array to prepend self without heap allocation.
99+ // self goes in tmp[0], remaining args follow.
100+ var tmp [maxArgs]uintptr
101+ tmp[0] = self
102+ copy(tmp[1:], args)
103+ return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
104+}
M syscall_32bit.go +21 -0
@@ -17,6 +17,7 @@ const (
1717 )
1818
1919 type syscall15Args struct {
20+ _ hostLayout
2021 fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
2122 a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr
2223 f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr
@@ -107,3 +108,23 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
107108 copy(tmp[:], args)
108109 return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
109110 }
111+
112+// SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
113+// It is designed for method-like calls where the first argument is always a receiver pointer.
114+// This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
115+//
116+//go:uintptrescapes
117+func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
118+ if fn == 0 {
119+ panic("purego: fn is nil")
120+ }
121+ if len(args) > maxArgs-1 {
122+ panic("purego: too many arguments to SyscallSelf")
123+ }
124+ // Use a stack-allocated array to prepend self without heap allocation.
125+ // self goes in tmp[0], remaining args follow.
126+ var tmp [maxArgs]uintptr
127+ tmp[0] = self
128+ copy(tmp[1:], args)
129+ return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
130+}
@@ -17,6 +17,7 @@ const (
17 )17 )
18 18
19 type syscall15Args struct {19 type syscall15Args struct {
20+ _ hostLayout
20 fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr21 fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
21 a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr22 a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr
22 f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr23 f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr
@@ -107,3 +108,23 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
107 copy(tmp[:], args)108 copy(tmp[:], args)
108 return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])109 return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
109 }110 }
111+
112+// SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
113+// It is designed for method-like calls where the first argument is always a receiver pointer.
114+// This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
115+//
116+//go:uintptrescapes
117+func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
118+ if fn == 0 {
119+ panic("purego: fn is nil")
120+ }
121+ if len(args) > maxArgs-1 {
122+ panic("purego: too many arguments to SyscallSelf")
123+ }
124+ // Use a stack-allocated array to prepend self without heap allocation.
125+ // self goes in tmp[0], remaining args follow.
126+ var tmp [maxArgs]uintptr
127+ tmp[0] = self
128+ copy(tmp[1:], args)
129+ return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
130+}
M syscall_notstackargs.go +1 -0
@@ -17,6 +17,7 @@ import "unsafe"
1717 // the runtime's own callback mechanism, so this type is compiled but unused,
1818 // serving only as a stub to satisfy cross-platform compilation.
1919 type callbackArgs struct {
20+ _ hostLayout
2021 index uintptr
2122 // args points to the argument block.
2223 //
@@ -17,6 +17,7 @@ import "unsafe"
17 // the runtime's own callback mechanism, so this type is compiled but unused,17 // the runtime's own callback mechanism, so this type is compiled but unused,
18 // serving only as a stub to satisfy cross-platform compilation.18 // serving only as a stub to satisfy cross-platform compilation.
19 type callbackArgs struct {19 type callbackArgs struct {
20+ _ hostLayout
20 index uintptr21 index uintptr
21 // args points to the argument block.22 // args points to the argument block.
22 //23 //
M syscall_stackargs.go +1 -0
@@ -17,6 +17,7 @@ import "unsafe"
1717 // the runtime's own callback mechanism, so this type is compiled but unused,
1818 // serving only as a stub to satisfy cross-platform compilation.
1919 type callbackArgs struct {
20+ _ hostLayout
2021 index uintptr
2122 // args points to the argument block.
2223 //
@@ -17,6 +17,7 @@ import "unsafe"
17 // the runtime's own callback mechanism, so this type is compiled but unused,17 // the runtime's own callback mechanism, so this type is compiled but unused,
18 // serving only as a stub to satisfy cross-platform compilation.18 // serving only as a stub to satisfy cross-platform compilation.
19 type callbackArgs struct {19 type callbackArgs struct {
20+ _ hostLayout
20 index uintptr21 index uintptr
21 // args points to the argument block.22 // args points to the argument block.
22 //23 //
M syscall_test.go +77 -0
@@ -4,6 +4,7 @@
44 package purego_test
55
66 import (
7+ "fmt"
78 "os"
89 "runtime"
910 "testing"
@@ -54,3 +55,79 @@ func TestErrno(t *testing.T) {
5455 t.Errorf("strerror returned %q, wanted \"%s\"", got, expected)
5556 }
5657 }
58+
59+func TestSyscallSelf(t *testing.T) {
60+ // Test that SyscallSelf(fn, self, args...) produces the same result as
61+ // SyscallN(fn, self, args...) for various argument counts.
62+
63+ // sum2 takes (self, a1) and returns self + a1
64+ sum2 := purego.NewCallback(func(self, a1 uintptr) uintptr {
65+ return self + a1
66+ })
67+ // sum4 takes (self, a1, a2, a3) and returns self + a1 + a2 + a3
68+ sum4 := purego.NewCallback(func(self, a1, a2, a3 uintptr) uintptr {
69+ return self + a1 + a2 + a3
70+ })
71+
72+ tests := []struct {
73+ name string
74+ fn uintptr
75+ self uintptr
76+ args []uintptr
77+ want uintptr
78+ }{
79+ {"no extra args", sum2, 10, []uintptr{20}, 30},
80+ {"zero self", sum2, 0, []uintptr{42}, 42},
81+ {"multiple args", sum4, 1, []uintptr{2, 3, 4}, 10},
82+ {"no variadic args",
83+ purego.NewCallback(func(self uintptr) uintptr { return self * 3 }),
84+ 7, nil, 21},
85+ }
86+
87+ for _, tc := range tests {
88+ t.Run(tc.name, func(t *testing.T) {
89+ r1, _, _ := purego.SyscallSelf(tc.fn, tc.self, tc.args...)
90+ if r1 != tc.want {
91+ t.Errorf("SyscallSelf returned %d, want %d", r1, tc.want)
92+ }
93+
94+ // Verify it matches SyscallN with manually prepended self
95+ allArgs := make([]uintptr, 0, 1+len(tc.args))
96+ allArgs = append(allArgs, tc.self)
97+ allArgs = append(allArgs, tc.args...)
98+ r1n, _, _ := purego.SyscallN(tc.fn, allArgs...)
99+ if r1 != r1n {
100+ t.Errorf("SyscallSelf result %d differs from SyscallN result %d", r1, r1n)
101+ }
102+ })
103+ }
104+}
105+
106+func TestSyscallSelfPanics(t *testing.T) {
107+ t.Run("nil fn", func(t *testing.T) {
108+ defer func() {
109+ if r := recover(); r == nil {
110+ t.Error("expected panic for nil fn")
111+ }
112+ }()
113+ purego.SyscallSelf(0, 1)
114+ })
115+
116+ t.Run("too many args", func(t *testing.T) {
117+ defer func() {
118+ r := recover()
119+ if r == nil {
120+ t.Error("expected panic for too many args")
121+ }
122+ msg := fmt.Sprint(r)
123+ if msg != "purego: too many arguments to SyscallSelf" {
124+ t.Errorf("unexpected panic message: %s", msg)
125+ }
126+ }()
127+ // Create a valid callback so we don't panic on nil fn
128+ fn := purego.NewCallback(func(uintptr) uintptr { return 0 })
129+ // Pass more args than maxArgs-1 allows (maxArgs is 15 on 64-bit)
130+ args := make([]uintptr, 15)
131+ purego.SyscallSelf(fn, 1, args...)
132+ })
133+}
@@ -4,6 +4,7 @@
4 package purego_test4 package purego_test
5 5
6 import (6 import (
7+ "fmt"
7 "os"8 "os"
8 "runtime"9 "runtime"
9 "testing"10 "testing"
@@ -54,3 +55,79 @@ func TestErrno(t *testing.T) {
54 t.Errorf("strerror returned %q, wanted \"%s\"", got, expected)55 t.Errorf("strerror returned %q, wanted \"%s\"", got, expected)
55 }56 }
56 }57 }
58+
59+func TestSyscallSelf(t *testing.T) {
60+ // Test that SyscallSelf(fn, self, args...) produces the same result as
61+ // SyscallN(fn, self, args...) for various argument counts.
62+
63+ // sum2 takes (self, a1) and returns self + a1
64+ sum2 := purego.NewCallback(func(self, a1 uintptr) uintptr {
65+ return self + a1
66+ })
67+ // sum4 takes (self, a1, a2, a3) and returns self + a1 + a2 + a3
68+ sum4 := purego.NewCallback(func(self, a1, a2, a3 uintptr) uintptr {
69+ return self + a1 + a2 + a3
70+ })
71+
72+ tests := []struct {
73+ name string
74+ fn uintptr
75+ self uintptr
76+ args []uintptr
77+ want uintptr
78+ }{
79+ {"no extra args", sum2, 10, []uintptr{20}, 30},
80+ {"zero self", sum2, 0, []uintptr{42}, 42},
81+ {"multiple args", sum4, 1, []uintptr{2, 3, 4}, 10},
82+ {"no variadic args",
83+ purego.NewCallback(func(self uintptr) uintptr { return self * 3 }),
84+ 7, nil, 21},
85+ }
86+
87+ for _, tc := range tests {
88+ t.Run(tc.name, func(t *testing.T) {
89+ r1, _, _ := purego.SyscallSelf(tc.fn, tc.self, tc.args...)
90+ if r1 != tc.want {
91+ t.Errorf("SyscallSelf returned %d, want %d", r1, tc.want)
92+ }
93+
94+ // Verify it matches SyscallN with manually prepended self
95+ allArgs := make([]uintptr, 0, 1+len(tc.args))
96+ allArgs = append(allArgs, tc.self)
97+ allArgs = append(allArgs, tc.args...)
98+ r1n, _, _ := purego.SyscallN(tc.fn, allArgs...)
99+ if r1 != r1n {
100+ t.Errorf("SyscallSelf result %d differs from SyscallN result %d", r1, r1n)
101+ }
102+ })
103+ }
104+}
105+
106+func TestSyscallSelfPanics(t *testing.T) {
107+ t.Run("nil fn", func(t *testing.T) {
108+ defer func() {
109+ if r := recover(); r == nil {
110+ t.Error("expected panic for nil fn")
111+ }
112+ }()
113+ purego.SyscallSelf(0, 1)
114+ })
115+
116+ t.Run("too many args", func(t *testing.T) {
117+ defer func() {
118+ r := recover()
119+ if r == nil {
120+ t.Error("expected panic for too many args")
121+ }
122+ msg := fmt.Sprint(r)
123+ if msg != "purego: too many arguments to SyscallSelf" {
124+ t.Errorf("unexpected panic message: %s", msg)
125+ }
126+ }()
127+ // Create a valid callback so we don't panic on nil fn
128+ fn := purego.NewCallback(func(uintptr) uintptr { return 0 })
129+ // Pass more args than maxArgs-1 allows (maxArgs is 15 on 64-bit)
130+ args := make([]uintptr, 15)
131+ purego.SyscallSelf(fn, 1, args...)
132+ })
133+}
M syscall_unix.go +23 -8
@@ -55,9 +55,10 @@ func NewCallback(fn any) uintptr {
5555 const maxCB = 2000
5656
5757 var cbs struct {
58- lock sync.Mutex
59- numFn int // the number of functions currently in cbs.funcs
60- funcs [maxCB]reflect.Value // the saved callbacks
58+ lock sync.RWMutex
59+ numFn int // the number of functions currently in cbs.funcs
60+ funcs [maxCB]reflect.Value // the saved callbacks
61+ argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback
6162 }
6263
6364 func compileCallback(fn any) uintptr {
@@ -103,9 +104,16 @@ output:
103104 if cbs.numFn >= maxCB {
104105 panic("purego: the maximum number of callbacks has been reached")
105106 }
106- cbs.funcs[cbs.numFn] = val
107+ index := cbs.numFn
108+ cbs.funcs[index] = val
109+ numIn := ty.NumIn()
110+ cbs.argPools[index] = &sync.Pool{
111+ New: func() any {
112+ return make([]reflect.Value, numIn)
113+ },
114+ }
107115 cbs.numFn++
108- return callbackasmAddr(cbs.numFn - 1)
116+ return callbackasmAddr(index)
109117 }
110118
111119 const ptrSize = unsafe.Sizeof((*int)(nil))
@@ -127,11 +135,18 @@ var callbackWrap_call = callbackWrap
127135 // callbackWrap is called by assembly code which determines which Go function to call.
128136 // This function takes the arguments and passes them to the Go function and returns the result.
129137 func callbackWrap(a *callbackArgs) {
130- cbs.lock.Lock()
138+ cbs.lock.RLock()
131139 fn := cbs.funcs[a.index]
132- cbs.lock.Unlock()
140+ pool := cbs.argPools[a.index]
141+ cbs.lock.RUnlock()
133142 fnType := fn.Type()
134- args := make([]reflect.Value, fnType.NumIn())
143+ args := pool.Get().([]reflect.Value)
144+ defer func() {
145+ for i := range args {
146+ args[i] = reflect.Value{}
147+ }
148+ pool.Put(args)
149+ }()
135150 frame := (*[callbackMaxFrame]uintptr)(a.args)
136151 // stackFrame points to stack-passed arguments. On most architectures this is
137152 // contiguous with frame (after register args), but on ppc64le it's separate.
@@ -55,9 +55,10 @@ func NewCallback(fn any) uintptr {
55 const maxCB = 200055 const maxCB = 2000
56 56
57 var cbs struct {57 var cbs struct {
58- lock sync.Mutex58+ lock sync.RWMutex
59- numFn int // the number of functions currently in cbs.funcs59+ numFn int // the number of functions currently in cbs.funcs
60- funcs [maxCB]reflect.Value // the saved callbacks60+ funcs [maxCB]reflect.Value // the saved callbacks
61+ argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback
61 }62 }
62 63
63 func compileCallback(fn any) uintptr {64 func compileCallback(fn any) uintptr {
@@ -103,9 +104,16 @@ output:
103 if cbs.numFn >= maxCB {104 if cbs.numFn >= maxCB {
104 panic("purego: the maximum number of callbacks has been reached")105 panic("purego: the maximum number of callbacks has been reached")
105 }106 }
106- cbs.funcs[cbs.numFn] = val107+ index := cbs.numFn
108+ cbs.funcs[index] = val
109+ numIn := ty.NumIn()
110+ cbs.argPools[index] = &sync.Pool{
111+ New: func() any {
112+ return make([]reflect.Value, numIn)
113+ },
114+ }
107 cbs.numFn++115 cbs.numFn++
108- return callbackasmAddr(cbs.numFn - 1)116+ return callbackasmAddr(index)
109 }117 }
110 118
111 const ptrSize = unsafe.Sizeof((*int)(nil))119 const ptrSize = unsafe.Sizeof((*int)(nil))
@@ -127,11 +135,18 @@ var callbackWrap_call = callbackWrap
127 // callbackWrap is called by assembly code which determines which Go function to call.135 // callbackWrap is called by assembly code which determines which Go function to call.
128 // This function takes the arguments and passes them to the Go function and returns the result.136 // This function takes the arguments and passes them to the Go function and returns the result.
129 func callbackWrap(a *callbackArgs) {137 func callbackWrap(a *callbackArgs) {
130- cbs.lock.Lock()138+ cbs.lock.RLock()
131 fn := cbs.funcs[a.index]139 fn := cbs.funcs[a.index]
132- cbs.lock.Unlock()140+ pool := cbs.argPools[a.index]
141+ cbs.lock.RUnlock()
133 fnType := fn.Type()142 fnType := fn.Type()
134- args := make([]reflect.Value, fnType.NumIn())143+ args := pool.Get().([]reflect.Value)
144+ defer func() {
145+ for i := range args {
146+ args[i] = reflect.Value{}
147+ }
148+ pool.Put(args)
149+ }()
135 frame := (*[callbackMaxFrame]uintptr)(a.args)150 frame := (*[callbackMaxFrame]uintptr)(a.args)
136 // stackFrame points to stack-passed arguments. On most architectures this is151 // stackFrame points to stack-passed arguments. On most architectures this is
137 // contiguous with frame (after register args), but on ppc64le it's separate.152 // contiguous with frame (after register args), but on ppc64le it's separate.

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