v0.11.0-alpha.6 d60df56

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	"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 i := 0; i < b.N; i++ {
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 i := 0; i < b.N; i++ {
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 i := 0; i < b.N; i++ {
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}

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