feat(syscall): add fixed-arity zero-allocation Syscall6 and Syscall15
- commit
- d3eb2c6d65215306eb3f38a2e71193aa1768c8b1
- author
- brice · · 2026-09-27 18:01 UTC
- committer
- brice · · 2026-09-27 18:45 UTC
- parent
- f844878
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +93 -0
M syscall_bench_test.go +44 -0
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "fmt" | |
| 8 | 8 | "os" | |
| 9 | 9 | "path/filepath" | |
| 10 | + | "runtime" | |
| 10 | 11 | "testing" | |
| 11 | 12 | ||
| 12 | 13 | "github.com/bnema/purego" | |
| @@ -146,6 +147,49 @@ func BenchmarkCallingMethods(b *testing.B) { | |||
| 146 | 147 | } | |
| 147 | 148 | }) | |
| 148 | 149 | ||
| 150 | + | // Fixed-arity integer calls compared with literal SyscallN arguments. | |
| 151 | + | b.Run("Syscall6", func(b *testing.B) { | |
| 152 | + | b.ReportAllocs() | |
| 153 | + | fn := testCases[3].cFnPtr | |
| 154 | + | var result uintptr | |
| 155 | + | b.ResetTimer() | |
| 156 | + | for range b.N { | |
| 157 | + | result, _, _ = purego.Syscall6(fn, 1, 2, 3, 4, 5, 0) | |
| 158 | + | } | |
| 159 | + | if result != 15 { | |
| 160 | + | b.Fatalf("sum = %d", result) | |
| 161 | + | } | |
| 162 | + | }) | |
| 163 | + | b.Run("SyscallNLiteral", func(b *testing.B) { | |
| 164 | + | b.ReportAllocs() | |
| 165 | + | fn := testCases[3].cFnPtr | |
| 166 | + | var result uintptr | |
| 167 | + | b.ResetTimer() | |
| 168 | + | for range b.N { | |
| 169 | + | result, _, _ = purego.SyscallN(fn, 1, 2, 3, 4, 5, 0) | |
| 170 | + | } | |
| 171 | + | if result != 15 { | |
| 172 | + | b.Fatalf("sum = %d", result) | |
| 173 | + | } | |
| 174 | + | }) | |
| 175 | + | if runtime.GOOS != "windows" { | |
| 176 | + | b.Run("CallbackInts", func(b *testing.B) { | |
| 177 | + | b.ReportAllocs() | |
| 178 | + | cb := purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr { | |
| 179 | + | return a.Int(0) + a.Int(1) + a.Int(2) + a.Int(3) + a.Int(4) | |
| 180 | + | }) | |
| 181 | + | fn := testCases[3].cCallbackPtr | |
| 182 | + | var result uintptr | |
| 183 | + | b.ResetTimer() | |
| 184 | + | for range b.N { | |
| 185 | + | result, _, _ = purego.Syscall6(fn, cb, 1, 2, 3, 4, 5) | |
| 186 | + | } | |
| 187 | + | if result != 15 { | |
| 188 | + | b.Fatalf("sum = %d", result) | |
| 189 | + | } | |
| 190 | + | }) | |
| 191 | + | } | |
| 192 | + | ||
| 149 | 193 | // Benchmark round-trip: Go → C → Go callback (realistic use case) | |
| 150 | 194 | b.Run("RoundTrip/GoC", func(b *testing.B) { | |
| 151 | 195 | for _, tc := range testCases { | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | "os" | 8 | "os" | ||
| 9 | "path/filepath" | 9 | "path/filepath" | ||
| 10 | + | "runtime" | |||
| 10 | "testing" | 11 | "testing" | ||
| 11 | 12 | ||||
| 12 | "github.com/bnema/purego" | 13 | "github.com/bnema/purego" | ||
| @@ -146,6 +147,49 @@ func BenchmarkCallingMethods(b *testing.B) { | |||||
| 146 | } | 147 | } | ||
| 147 | }) | 148 | }) | ||
| 148 | 149 | ||||
| 150 | + | // Fixed-arity integer calls compared with literal SyscallN arguments. | |||
| 151 | + | b.Run("Syscall6", func(b *testing.B) { | |||
| 152 | + | b.ReportAllocs() | |||
| 153 | + | fn := testCases[3].cFnPtr | |||
| 154 | + | var result uintptr | |||
| 155 | + | b.ResetTimer() | |||
| 156 | + | for range b.N { | |||
| 157 | + | result, _, _ = purego.Syscall6(fn, 1, 2, 3, 4, 5, 0) | |||
| 158 | + | } | |||
| 159 | + | if result != 15 { | |||
| 160 | + | b.Fatalf("sum = %d", result) | |||
| 161 | + | } | |||
| 162 | + | }) | |||
| 163 | + | b.Run("SyscallNLiteral", func(b *testing.B) { | |||
| 164 | + | b.ReportAllocs() | |||
| 165 | + | fn := testCases[3].cFnPtr | |||
| 166 | + | var result uintptr | |||
| 167 | + | b.ResetTimer() | |||
| 168 | + | for range b.N { | |||
| 169 | + | result, _, _ = purego.SyscallN(fn, 1, 2, 3, 4, 5, 0) | |||
| 170 | + | } | |||
| 171 | + | if result != 15 { | |||
| 172 | + | b.Fatalf("sum = %d", result) | |||
| 173 | + | } | |||
| 174 | + | }) | |||
| 175 | + | if runtime.GOOS != "windows" { | |||
| 176 | + | b.Run("CallbackInts", func(b *testing.B) { | |||
| 177 | + | b.ReportAllocs() | |||
| 178 | + | cb := purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr { | |||
| 179 | + | return a.Int(0) + a.Int(1) + a.Int(2) + a.Int(3) + a.Int(4) | |||
| 180 | + | }) | |||
| 181 | + | fn := testCases[3].cCallbackPtr | |||
| 182 | + | var result uintptr | |||
| 183 | + | b.ResetTimer() | |||
| 184 | + | for range b.N { | |||
| 185 | + | result, _, _ = purego.Syscall6(fn, cb, 1, 2, 3, 4, 5) | |||
| 186 | + | } | |||
| 187 | + | if result != 15 { | |||
| 188 | + | b.Fatalf("sum = %d", result) | |||
| 189 | + | } | |||
| 190 | + | }) | |||
| 191 | + | } | |||
| 192 | + | ||||
| 149 | // Benchmark round-trip: Go → C → Go callback (realistic use case) | 193 | // Benchmark round-trip: Go → C → Go callback (realistic use case) | ||
| 150 | b.Run("RoundTrip/GoC", func(b *testing.B) { | 194 | b.Run("RoundTrip/GoC", func(b *testing.B) { | ||
| 151 | for _, tc := range testCases { | 195 | for _, tc := range testCases { | ||
A syscall_fixed.go +49 -0
| @@ -0,0 +1,49 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2026 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | //go:build darwin || freebsd || linux || netbsd || windows | |
| 5 | + | ||
| 6 | + | package purego | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "runtime" | |
| 10 | + | "unsafe" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | //go:noescape | |
| 14 | + | //go:linkname noescapeFixed runtime.noescape | |
| 15 | + | func noescapeFixed(p unsafe.Pointer) unsafe.Pointer | |
| 16 | + | ||
| 17 | + | // Syscall15 calls fn with up to 15 integer-class arguments without | |
| 18 | + | // allocating. Unused arguments must be 0. Unlike SyscallN it does not use a | |
| 19 | + | // variadic argument slice. Like SyscallN, a uintptr converted from an | |
| 20 | + | // unsafe.Pointer in the call expression keeps its object alive and un-moved | |
| 21 | + | // for the duration of the call (go:uintptrescapes); the object escapes to | |
| 22 | + | // the heap. The C function must not retain the pointer after returning. | |
| 23 | + | // Mixed integer/float arguments are not supported, as for SyscallN. | |
| 24 | + | // | |
| 25 | + | //go:uintptrescapes | |
| 26 | + | func Syscall15(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) { | |
| 27 | + | if fn == 0 { | |
| 28 | + | panic("purego: fn is nil") | |
| 29 | + | } | |
| 30 | + | if runtime.GOOS == "windows" { | |
| 31 | + | return syscall_syscallN(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) | |
| 32 | + | } | |
| 33 | + | s := syscallArgs{ | |
| 34 | + | fn: fn, a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, | |
| 35 | + | a6: a6, a7: a7, a8: a8, a9: a9, a10: a10, | |
| 36 | + | a11: a11, a12: a12, a13: a13, a14: a14, a15: a15, | |
| 37 | + | } | |
| 38 | + | // cgocall is synchronous: s remains on this goroutine's stack until it returns. | |
| 39 | + | runtime_cgocall(syscallXABI0, noescapeFixed(unsafe.Pointer(&s))) | |
| 40 | + | return s.a1, s.a2, s.a3 | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // Syscall6 is Syscall15 with six arguments. Like Syscall15, pointer-derived | |
| 44 | + | // uintptr arguments keep their objects alive and cause them to escape. | |
| 45 | + | // | |
| 46 | + | //go:uintptrescapes | |
| 47 | + | func Syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) { | |
| 48 | + | return Syscall15(fn, a1, a2, a3, a4, a5, a6, 0, 0, 0, 0, 0, 0, 0, 0, 0) | |
| 49 | + | } | |
| @@ -0,0 +1,49 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2026 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | //go:build darwin || freebsd || linux || netbsd || windows | |||
| 5 | + | ||||
| 6 | + | package purego | |||
| 7 | + | ||||
| 8 | + | import ( | |||
| 9 | + | "runtime" | |||
| 10 | + | "unsafe" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | //go:noescape | |||
| 14 | + | //go:linkname noescapeFixed runtime.noescape | |||
| 15 | + | func noescapeFixed(p unsafe.Pointer) unsafe.Pointer | |||
| 16 | + | ||||
| 17 | + | // Syscall15 calls fn with up to 15 integer-class arguments without | |||
| 18 | + | // allocating. Unused arguments must be 0. Unlike SyscallN it does not use a | |||
| 19 | + | // variadic argument slice. Like SyscallN, a uintptr converted from an | |||
| 20 | + | // unsafe.Pointer in the call expression keeps its object alive and un-moved | |||
| 21 | + | // for the duration of the call (go:uintptrescapes); the object escapes to | |||
| 22 | + | // the heap. The C function must not retain the pointer after returning. | |||
| 23 | + | // Mixed integer/float arguments are not supported, as for SyscallN. | |||
| 24 | + | // | |||
| 25 | + | //go:uintptrescapes | |||
| 26 | + | func Syscall15(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) { | |||
| 27 | + | if fn == 0 { | |||
| 28 | + | panic("purego: fn is nil") | |||
| 29 | + | } | |||
| 30 | + | if runtime.GOOS == "windows" { | |||
| 31 | + | return syscall_syscallN(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) | |||
| 32 | + | } | |||
| 33 | + | s := syscallArgs{ | |||
| 34 | + | fn: fn, a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, | |||
| 35 | + | a6: a6, a7: a7, a8: a8, a9: a9, a10: a10, | |||
| 36 | + | a11: a11, a12: a12, a13: a13, a14: a14, a15: a15, | |||
| 37 | + | } | |||
| 38 | + | // cgocall is synchronous: s remains on this goroutine's stack until it returns. | |||
| 39 | + | runtime_cgocall(syscallXABI0, noescapeFixed(unsafe.Pointer(&s))) | |||
| 40 | + | return s.a1, s.a2, s.a3 | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // Syscall6 is Syscall15 with six arguments. Like Syscall15, pointer-derived | |||
| 44 | + | // uintptr arguments keep their objects alive and cause them to escape. | |||
| 45 | + | // | |||
| 46 | + | //go:uintptrescapes | |||
| 47 | + | func Syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) { | |||
| 48 | + | return Syscall15(fn, a1, a2, a3, a4, a5, a6, 0, 0, 0, 0, 0, 0, 0, 0, 0) | |||
| 49 | + | } | |||