1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3
4package purego_test
5
6import (
7 "fmt"
8 "os"
9 "path/filepath"
10 "runtime"
11 "testing"
12
13 "github.com/bnema/purego"
14 "github.com/bnema/purego/internal/load"
15)
16
17// BenchmarkCallingMethods compares RegisterFunc, SyscallN, and Callback methods
18func BenchmarkCallingMethods(b *testing.B) {
19 testCases := []struct {
20 n int
21 goFn any
22 goFnPtr uintptr
23 cFnPtr uintptr
24 cFnName string
25 cCallbackPtr uintptr
26 cCallbackName string
27 args []int64
28 expectedSum int64
29 }{
30 {1, goSum1, 0, 0, "sum1_c", 0, "call_callback1", []int64{1}, 1},
31 {2, goSum2, 0, 0, "sum2_c", 0, "call_callback2", []int64{1, 2}, 3},
32 {3, goSum3, 0, 0, "sum3_c", 0, "call_callback3", []int64{1, 2, 3}, 6},
33 {5, goSum5, 0, 0, "sum5_c", 0, "call_callback5", []int64{1, 2, 3, 4, 5}, 15},
34 {10, goSum10, 0, 0, "sum10_c", 0, "call_callback10", []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 55},
35 {14, goSum15, 0, 0, "sum14_c", 0, "call_callback14", []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, 105},
36 {15, goSum15, 0, 0, "sum15_c", 0, "call_callback15", []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, 120},
37 }
38
39 // Build C library for benchmarking
40 libFileName := filepath.Join(b.TempDir(), "libbenchmark.so")
41 if err := buildSharedLib(b, "CC", libFileName, filepath.Join("testdata", "benchmarktest", "benchmark.c")); err != nil {
42 b.Fatalf("Failed to build C library: %v", err)
43 }
44 b.Cleanup(func() {
45 os.Remove(libFileName)
46 })
47
48 libHandle, err := load.OpenLibrary(libFileName)
49 if err != nil {
50 b.Fatalf("Failed to load C library: %v", err)
51 }
52 b.Cleanup(func() {
53 if err := load.CloseLibrary(libHandle); err != nil {
54 b.Fatalf("Failed to close library: %s", err)
55 }
56 })
57
58 // Create callbacks and load C functions
59 for i := range testCases {
60 testCases[i].goFnPtr = purego.NewCallback(testCases[i].goFn)
61
62 cFn, err := load.OpenSymbol(libHandle, testCases[i].cFnName)
63 if err != nil {
64 b.Fatalf("Failed to load C function %s: %v", testCases[i].cFnName, err)
65 }
66 testCases[i].cFnPtr = cFn
67
68 cCallbackFn, err := load.OpenSymbol(libHandle, testCases[i].cCallbackName)
69 if err != nil {
70 b.Fatalf("Failed to load C callback wrapper %s: %v", testCases[i].cCallbackName, err)
71 }
72 testCases[i].cCallbackPtr = cCallbackFn
73 }
74
75 b.Run("RegisterFunc/Callback", func(b *testing.B) {
76 for _, tc := range testCases {
77 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
78 b.ReportAllocs()
79 registerFn := makeRegisterFunc(tc.n)
80 purego.RegisterFunc(registerFn, tc.goFnPtr)
81
82 b.ResetTimer()
83 result := callRegisterFunc(registerFn, tc.n, tc.args, b.N)
84 b.StopTimer()
85
86 if result != tc.expectedSum {
87 b.Fatalf("RegisterFunc/Callback: expected sum %d, got %d", tc.expectedSum, result)
88 }
89 })
90 }
91 })
92
93 // Benchmark RegisterFunc with C functions
94 b.Run("RegisterFunc/CFunc", func(b *testing.B) {
95 for _, tc := range testCases {
96 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
97 b.ReportAllocs()
98 registerFn := makeRegisterFunc(tc.n)
99 purego.RegisterFunc(registerFn, tc.cFnPtr)
100
101 b.ResetTimer()
102 result := callRegisterFunc(registerFn, tc.n, tc.args, b.N)
103 b.StopTimer()
104
105 if result != tc.expectedSum {
106 b.Fatalf("RegisterFunc/CFunc: expected sum %d, got %d", tc.expectedSum, result)
107 }
108 })
109 }
110 })
111
112 // Benchmark SyscallN with Go callbacks
113 b.Run("SyscallN/Callback", func(b *testing.B) {
114 for _, tc := range testCases {
115 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
116 b.ReportAllocs()
117 args := int64sToUintptrs(tc.args)
118 var result uintptr
119 b.ResetTimer()
120 for range b.N {
121 result, _, _ = purego.SyscallN(tc.goFnPtr, args...)
122 }
123 b.StopTimer()
124 if int64(result) != tc.expectedSum {
125 b.Fatalf("SyscallN/Callback: expected sum %d, got %d", tc.expectedSum, result)
126 }
127 })
128 }
129 })
130
131 // Benchmark SyscallN with C functions
132 b.Run("SyscallN/CFunc", func(b *testing.B) {
133 for _, tc := range testCases {
134 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
135 b.ReportAllocs()
136 args := int64sToUintptrs(tc.args)
137 var result uintptr
138 b.ResetTimer()
139 for range b.N {
140 result, _, _ = purego.SyscallN(tc.cFnPtr, args...)
141 }
142 b.StopTimer()
143 if int64(result) != tc.expectedSum {
144 b.Fatalf("SyscallN/CFunc: expected sum %d, got %d", tc.expectedSum, result)
145 }
146 })
147 }
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
193 // Benchmark round-trip: Go → C → Go callback (realistic use case)
194 b.Run("RoundTrip/GoC", func(b *testing.B) {
195 for _, tc := range testCases {
196 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
197 b.ReportAllocs()
198 // Build args: first arg is callback pointer, rest are the arguments
199 args := int64sToUintptrs(tc.args)
200 callbackArgs := make([]uintptr, len(args)+1)
201 callbackArgs[0] = tc.goFnPtr
202 copy(callbackArgs[1:], args)
203
204 // Skip if total args (callback + args) exceeds or meets limit
205 // SyscallN has issues with exactly 15 or more arguments
206 if len(callbackArgs) >= 15 {
207 b.Skipf("Round-trip with %d args + callback (%d total) exceeds/meets SyscallN limit", tc.n, len(callbackArgs))
208 }
209
210 var result uintptr
211 b.ResetTimer()
212 for range b.N {
213 result, _, _ = purego.SyscallN(tc.cCallbackPtr, callbackArgs...)
214 }
215 b.StopTimer()
216 if int64(result) != tc.expectedSum {
217 b.Fatalf("RoundTrip: expected sum %d, got %d", tc.expectedSum, result)
218 }
219 })
220 }
221 })
222}
223
224// makeRegisterFunc creates a function pointer of the appropriate signature
225func makeRegisterFunc(n int) any {
226 switch n {
227 case 1:
228 return new(func(int64) int64)
229 case 2:
230 return new(func(int64, int64) int64)
231 case 3:
232 return new(func(int64, int64, int64) int64)
233 case 5:
234 return new(func(int64, int64, int64, int64, int64) int64)
235 case 10:
236 return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
237 case 14:
238 return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
239 case 15:
240 return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
241 default:
242 panic(fmt.Sprintf("unsupported arg count: %d", n))
243 }
244}
245
246// callRegisterFunc calls the registered function with the appropriate number of arguments
247func callRegisterFunc(registerFn any, n int, args []int64, iterations int) int64 {
248 var result int64
249 switch n {
250 case 1:
251 f := registerFn.(*func(int64) int64)
252 for range iterations {
253 result = (*f)(args[0])
254 }
255 case 2:
256 f := registerFn.(*func(int64, int64) int64)
257 for range iterations {
258 result = (*f)(args[0], args[1])
259 }
260 case 3:
261 f := registerFn.(*func(int64, int64, int64) int64)
262 for range iterations {
263 result = (*f)(args[0], args[1], args[2])
264 }
265 case 5:
266 f := registerFn.(*func(int64, int64, int64, int64, int64) int64)
267 for range iterations {
268 result = (*f)(args[0], args[1], args[2], args[3], args[4])
269 }
270 case 10:
271 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
272 for range iterations {
273 result = (*f)(args[0], args[1], args[2], args[3], args[4],
274 args[5], args[6], args[7], args[8], args[9])
275 }
276 case 14:
277 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
278 for range iterations {
279 result = (*f)(args[0], args[1], args[2], args[3], args[4],
280 args[5], args[6], args[7], args[8], args[9],
281 args[10], args[11], args[12], args[13])
282 }
283 case 15:
284 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
285 for range iterations {
286 result = (*f)(args[0], args[1], args[2], args[3], args[4],
287 args[5], args[6], args[7], args[8], args[9],
288 args[10], args[11], args[12], args[13], args[14])
289 }
290 default:
291 panic(fmt.Sprintf("unsupported arg count: %d", n))
292 }
293 return result
294}
295
296// int64sToUintptrs converts []int64 to []uintptr for SyscallN
297func int64sToUintptrs(args []int64) []uintptr {
298 result := make([]uintptr, len(args))
299 for i, v := range args {
300 result[i] = uintptr(v)
301 }
302 return result
303}
304
305func goSum1(a1 int64) int64 { return a1 }
306
307func goSum2(a1, a2 int64) int64 { return a1 + a2 }
308
309func goSum3(a1, a2, a3 int64) int64 { return a1 + a2 + a3 }
310
311func goSum5(a1, a2, a3, a4, a5 int64) int64 { return a1 + a2 + a3 + a4 + a5 }
312
313func goSum10(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 int64) int64 {
314 return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10
315}
316
317func goSum15(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 int64) int64 {
318 return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15
319}