v0.13.0-bnema.2 3a10209

syscall_bench_test.go RawHistoryFolder

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}

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