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 "testing"
11
12 "github.com/ebitengine/purego"
13 "github.com/ebitengine/purego/internal/load"
14)
15
16// BenchmarkCallingMethods compares RegisterFunc, SyscallN, and Callback methods
17func BenchmarkCallingMethods(b *testing.B) {
18 testCases := []struct {
19 n int
20 goFn any
21 goFnPtr uintptr
22 cFnPtr uintptr
23 cFnName string
24 cCallbackPtr uintptr
25 cCallbackName string
26 args []int64
27 expectedSum int64
28 }{
29 {1, goSum1, 0, 0, "sum1_c", 0, "call_callback1", []int64{1}, 1},
30 {2, goSum2, 0, 0, "sum2_c", 0, "call_callback2", []int64{1, 2}, 3},
31 {3, goSum3, 0, 0, "sum3_c", 0, "call_callback3", []int64{1, 2, 3}, 6},
32 {5, goSum5, 0, 0, "sum5_c", 0, "call_callback5", []int64{1, 2, 3, 4, 5}, 15},
33 {10, goSum10, 0, 0, "sum10_c", 0, "call_callback10", []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 55},
34 {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},
35 {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},
36 }
37
38 // Build C library for benchmarking
39 libFileName := filepath.Join(b.TempDir(), "libbenchmark.so")
40 if err := buildSharedLib(b, "CC", libFileName, filepath.Join("testdata", "benchmarktest", "benchmark.c")); err != nil {
41 b.Fatalf("Failed to build C library: %v", err)
42 }
43 b.Cleanup(func() {
44 os.Remove(libFileName)
45 })
46
47 libHandle, err := load.OpenLibrary(libFileName)
48 if err != nil {
49 b.Fatalf("Failed to load C library: %v", err)
50 }
51 b.Cleanup(func() {
52 if err := load.CloseLibrary(libHandle); err != nil {
53 b.Fatalf("Failed to close library: %s", err)
54 }
55 })
56
57 // Create callbacks and load C functions
58 for i := range testCases {
59 testCases[i].goFnPtr = purego.NewCallback(testCases[i].goFn)
60
61 cFn, err := load.OpenSymbol(libHandle, testCases[i].cFnName)
62 if err != nil {
63 b.Fatalf("Failed to load C function %s: %v", testCases[i].cFnName, err)
64 }
65 testCases[i].cFnPtr = cFn
66
67 cCallbackFn, err := load.OpenSymbol(libHandle, testCases[i].cCallbackName)
68 if err != nil {
69 b.Fatalf("Failed to load C callback wrapper %s: %v", testCases[i].cCallbackName, err)
70 }
71 testCases[i].cCallbackPtr = cCallbackFn
72 }
73
74 b.Run("RegisterFunc/Callback", func(b *testing.B) {
75 for _, tc := range testCases {
76 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
77 b.ReportAllocs()
78 registerFn := makeRegisterFunc(tc.n)
79 purego.RegisterFunc(registerFn, tc.goFnPtr)
80
81 b.ResetTimer()
82 result := callRegisterFunc(registerFn, tc.n, tc.args, b.N)
83 b.StopTimer()
84
85 if result != tc.expectedSum {
86 b.Fatalf("RegisterFunc/Callback: expected sum %d, got %d", tc.expectedSum, result)
87 }
88 })
89 }
90 })
91
92 // Benchmark RegisterFunc with C functions
93 b.Run("RegisterFunc/CFunc", func(b *testing.B) {
94 for _, tc := range testCases {
95 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
96 b.ReportAllocs()
97 registerFn := makeRegisterFunc(tc.n)
98 purego.RegisterFunc(registerFn, tc.cFnPtr)
99
100 b.ResetTimer()
101 result := callRegisterFunc(registerFn, tc.n, tc.args, b.N)
102 b.StopTimer()
103
104 if result != tc.expectedSum {
105 b.Fatalf("RegisterFunc/CFunc: expected sum %d, got %d", tc.expectedSum, result)
106 }
107 })
108 }
109 })
110
111 // Benchmark SyscallN with Go callbacks
112 b.Run("SyscallN/Callback", func(b *testing.B) {
113 for _, tc := range testCases {
114 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
115 b.ReportAllocs()
116 args := int64sToUintptrs(tc.args)
117 var result uintptr
118 b.ResetTimer()
119 for range b.N {
120 result, _, _ = purego.SyscallN(tc.goFnPtr, args...)
121 }
122 b.StopTimer()
123 if int64(result) != tc.expectedSum {
124 b.Fatalf("SyscallN/Callback: expected sum %d, got %d", tc.expectedSum, result)
125 }
126 })
127 }
128 })
129
130 // Benchmark SyscallN with C functions
131 b.Run("SyscallN/CFunc", func(b *testing.B) {
132 for _, tc := range testCases {
133 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
134 b.ReportAllocs()
135 args := int64sToUintptrs(tc.args)
136 var result uintptr
137 b.ResetTimer()
138 for range b.N {
139 result, _, _ = purego.SyscallN(tc.cFnPtr, args...)
140 }
141 b.StopTimer()
142 if int64(result) != tc.expectedSum {
143 b.Fatalf("SyscallN/CFunc: expected sum %d, got %d", tc.expectedSum, result)
144 }
145 })
146 }
147 })
148
149 // Benchmark round-trip: Go → C → Go callback (realistic use case)
150 b.Run("RoundTrip/GoC", func(b *testing.B) {
151 for _, tc := range testCases {
152 b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
153 b.ReportAllocs()
154 // Build args: first arg is callback pointer, rest are the arguments
155 args := int64sToUintptrs(tc.args)
156 callbackArgs := make([]uintptr, len(args)+1)
157 callbackArgs[0] = tc.goFnPtr
158 copy(callbackArgs[1:], args)
159
160 // Skip if total args (callback + args) exceeds or meets limit
161 // SyscallN has issues with exactly 15 or more arguments
162 if len(callbackArgs) >= 15 {
163 b.Skipf("Round-trip with %d args + callback (%d total) exceeds/meets SyscallN limit", tc.n, len(callbackArgs))
164 }
165
166 var result uintptr
167 b.ResetTimer()
168 for range b.N {
169 result, _, _ = purego.SyscallN(tc.cCallbackPtr, callbackArgs...)
170 }
171 b.StopTimer()
172 if int64(result) != tc.expectedSum {
173 b.Fatalf("RoundTrip: expected sum %d, got %d", tc.expectedSum, result)
174 }
175 })
176 }
177 })
178}
179
180// makeRegisterFunc creates a function pointer of the appropriate signature
181func makeRegisterFunc(n int) any {
182 switch n {
183 case 1:
184 return new(func(int64) int64)
185 case 2:
186 return new(func(int64, int64) int64)
187 case 3:
188 return new(func(int64, int64, int64) int64)
189 case 5:
190 return new(func(int64, int64, int64, int64, int64) int64)
191 case 10:
192 return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
193 case 14:
194 return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
195 case 15:
196 return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
197 default:
198 panic(fmt.Sprintf("unsupported arg count: %d", n))
199 }
200}
201
202// callRegisterFunc calls the registered function with the appropriate number of arguments
203func callRegisterFunc(registerFn any, n int, args []int64, iterations int) int64 {
204 var result int64
205 switch n {
206 case 1:
207 f := registerFn.(*func(int64) int64)
208 for range iterations {
209 result = (*f)(args[0])
210 }
211 case 2:
212 f := registerFn.(*func(int64, int64) int64)
213 for range iterations {
214 result = (*f)(args[0], args[1])
215 }
216 case 3:
217 f := registerFn.(*func(int64, int64, int64) int64)
218 for range iterations {
219 result = (*f)(args[0], args[1], args[2])
220 }
221 case 5:
222 f := registerFn.(*func(int64, int64, int64, int64, int64) int64)
223 for range iterations {
224 result = (*f)(args[0], args[1], args[2], args[3], args[4])
225 }
226 case 10:
227 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
228 for range iterations {
229 result = (*f)(args[0], args[1], args[2], args[3], args[4],
230 args[5], args[6], args[7], args[8], args[9])
231 }
232 case 14:
233 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
234 for range iterations {
235 result = (*f)(args[0], args[1], args[2], args[3], args[4],
236 args[5], args[6], args[7], args[8], args[9],
237 args[10], args[11], args[12], args[13])
238 }
239 case 15:
240 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
241 for range iterations {
242 result = (*f)(args[0], args[1], args[2], args[3], args[4],
243 args[5], args[6], args[7], args[8], args[9],
244 args[10], args[11], args[12], args[13], args[14])
245 }
246 default:
247 panic(fmt.Sprintf("unsupported arg count: %d", n))
248 }
249 return result
250}
251
252// int64sToUintptrs converts []int64 to []uintptr for SyscallN
253func int64sToUintptrs(args []int64) []uintptr {
254 result := make([]uintptr, len(args))
255 for i, v := range args {
256 result[i] = uintptr(v)
257 }
258 return result
259}
260
261func goSum1(a1 int64) int64 { return a1 }
262
263func goSum2(a1, a2 int64) int64 { return a1 + a2 }
264
265func goSum3(a1, a2, a3 int64) int64 { return a1 + a2 + a3 }
266
267func goSum5(a1, a2, a3, a4, a5 int64) int64 { return a1 + a2 + a3 + a4 + a5 }
268
269func goSum10(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 int64) int64 {
270 return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10
271}
272
273func goSum15(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 int64) int64 {
274 return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15
275}