detached c0b72dd

purego: add SyscallSelf for allocation-free method calls

commit
c0b72ddfe6853f0d75aa30547052a9ca44a3eb5c
author
brice · · 2026-09-27 05:54 UTC
parent
5509d1a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +118 -11
M syscall.go +9 -5
@@ -79,14 +79,18 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
7979 panic("purego: too many arguments to SyscallN")
8080 }
8181
82+ // Add padding so there is no out-of-bounds slicing.
83+ var tmp [maxArgs]uintptr
84+ n := copy(tmp[:], args)
85+ return syscallPadded(fn, &tmp, n)
86+}
87+
88+// syscallPadded calls fn with the first n values of tmp as arguments.
89+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
8290 // Windows uses syscall.SyscallN in syscall_windows.go.
8391 if runtime.GOOS == "windows" {
84- return syscall_syscallN(fn, args...)
92+ return syscall_syscallN(fn, tmp[:n]...)
8593 }
86-
87- // add padding so there is no out-of-bounds slicing
88- var tmp [maxArgs]uintptr
89- copy(tmp[:], args)
9094 var floats [maxArgs]uintptr
9195 copy(floats[:], tmp[:])
9296 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
@@ -79,14 +79,18 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
79 panic("purego: too many arguments to SyscallN")79 panic("purego: too many arguments to SyscallN")
80 }80 }
81 81
82+ // Add padding so there is no out-of-bounds slicing.
83+ var tmp [maxArgs]uintptr
84+ n := copy(tmp[:], args)
85+ return syscallPadded(fn, &tmp, n)
86+}
87+
88+// syscallPadded calls fn with the first n values of tmp as arguments.
89+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
82 // Windows uses syscall.SyscallN in syscall_windows.go.90 // Windows uses syscall.SyscallN in syscall_windows.go.
83 if runtime.GOOS == "windows" {91 if runtime.GOOS == "windows" {
84- return syscall_syscallN(fn, args...)92+ return syscall_syscallN(fn, tmp[:n]...)
85 }93 }
86-
87- // add padding so there is no out-of-bounds slicing
88- var tmp [maxArgs]uintptr
89- copy(tmp[:], args)
90 var floats [maxArgs]uintptr94 var floats [maxArgs]uintptr
91 copy(floats[:], tmp[:])95 copy(floats[:], tmp[:])
92 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)96 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
M syscall_32bit.go +9 -5
@@ -81,14 +81,18 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
8181 panic("purego: too many arguments to SyscallN")
8282 }
8383
84+ // Add padding so there is no out-of-bounds slicing.
85+ var tmp [maxArgs]uintptr
86+ n := copy(tmp[:], args)
87+ return syscallPadded(fn, &tmp, n)
88+}
89+
90+// syscallPadded calls fn with the first n values of tmp as arguments.
91+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
8492 // Windows uses syscall.SyscallN in syscall_windows.go.
8593 if runtime.GOOS == "windows" {
86- return syscall_syscallN(fn, args...)
94+ return syscall_syscallN(fn, tmp[:n]...)
8795 }
88-
89- // Add padding so there is no out-of-bounds slicing.
90- var tmp [maxArgs]uintptr
91- copy(tmp[:], args)
9296 var floats [16]uintptr
9397 copy(floats[:], tmp[:16])
9498 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
@@ -81,14 +81,18 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
81 panic("purego: too many arguments to SyscallN")81 panic("purego: too many arguments to SyscallN")
82 }82 }
83 83
84+ // Add padding so there is no out-of-bounds slicing.
85+ var tmp [maxArgs]uintptr
86+ n := copy(tmp[:], args)
87+ return syscallPadded(fn, &tmp, n)
88+}
89+
90+// syscallPadded calls fn with the first n values of tmp as arguments.
91+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
84 // Windows uses syscall.SyscallN in syscall_windows.go.92 // Windows uses syscall.SyscallN in syscall_windows.go.
85 if runtime.GOOS == "windows" {93 if runtime.GOOS == "windows" {
86- return syscall_syscallN(fn, args...)94+ return syscall_syscallN(fn, tmp[:n]...)
87 }95 }
88-
89- // Add padding so there is no out-of-bounds slicing.
90- var tmp [maxArgs]uintptr
91- copy(tmp[:], args)
92 var floats [16]uintptr96 var floats [16]uintptr
93 copy(floats[:], tmp[:16])97 copy(floats[:], tmp[:16])
94 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)98 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
M syscall_ppc64le.go +7 -1
@@ -63,8 +63,14 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
6363 panic("purego: too many arguments to SyscallN")
6464 }
6565
66+ // Add padding so there is no out-of-bounds slicing.
6667 var tmp [maxArgs]uintptr
67- copy(tmp[:], args)
68+ n := copy(tmp[:], args)
69+ return syscallPadded(fn, &tmp, n)
70+}
71+
72+// syscallPadded calls fn with the first n values of tmp as arguments.
73+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
6874 var floats [maxArgs]uintptr
6975 copy(floats[:], tmp[:])
7076 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
@@ -63,8 +63,14 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
63 panic("purego: too many arguments to SyscallN")63 panic("purego: too many arguments to SyscallN")
64 }64 }
65 65
66+ // Add padding so there is no out-of-bounds slicing.
66 var tmp [maxArgs]uintptr67 var tmp [maxArgs]uintptr
67- copy(tmp[:], args)68+ n := copy(tmp[:], args)
69+ return syscallPadded(fn, &tmp, n)
70+}
71+
72+// syscallPadded calls fn with the first n values of tmp as arguments.
73+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
68 var floats [maxArgs]uintptr74 var floats [maxArgs]uintptr
69 copy(floats[:], tmp[:])75 copy(floats[:], tmp[:])
70 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)76 s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
A syscall_self.go +24 -0
@@ -0,0 +1,24 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd || windows
5+
6+package purego
7+
8+// SyscallSelf calls fn with self as the first argument followed by args.
9+// It is equivalent to SyscallN(fn, append([]uintptr{self}, args...)...)
10+// without allocating the combined argument slice.
11+//
12+//go:uintptrescapes
13+func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
14+ if fn == 0 {
15+ panic("purego: fn is nil")
16+ }
17+ if len(args) > maxArgs-1 {
18+ panic("purego: too many arguments to SyscallSelf")
19+ }
20+ var tmp [maxArgs]uintptr
21+ tmp[0] = self
22+ n := copy(tmp[1:], args)
23+ return syscallPadded(fn, &tmp, n+1)
24+}
@@ -0,0 +1,24 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd || windows
5+
6+package purego
7+
8+// SyscallSelf calls fn with self as the first argument followed by args.
9+// It is equivalent to SyscallN(fn, append([]uintptr{self}, args...)...)
10+// without allocating the combined argument slice.
11+//
12+//go:uintptrescapes
13+func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
14+ if fn == 0 {
15+ panic("purego: fn is nil")
16+ }
17+ if len(args) > maxArgs-1 {
18+ panic("purego: too many arguments to SyscallSelf")
19+ }
20+ var tmp [maxArgs]uintptr
21+ tmp[0] = self
22+ n := copy(tmp[1:], args)
23+ return syscallPadded(fn, &tmp, n+1)
24+}
A syscall_self_test.go +69 -0
@@ -0,0 +1,69 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd || windows
5+
6+package purego_test
7+
8+import (
9+ "fmt"
10+ "testing"
11+
12+ "github.com/bnema/purego"
13+)
14+
15+func TestSyscallSelf(t *testing.T) {
16+ sum2 := purego.NewCallback(func(self, a1 uintptr) uintptr { return self + a1 })
17+ sum4 := purego.NewCallback(func(self, a1, a2, a3 uintptr) uintptr { return self + a1 + a2 + a3 })
18+ triple := purego.NewCallback(func(self uintptr) uintptr { return self * 3 })
19+
20+ tests := []struct {
21+ name string
22+ fn uintptr
23+ self uintptr
24+ args []uintptr
25+ want uintptr
26+ }{
27+ {"one arg", sum2, 10, []uintptr{20}, 30},
28+ {"zero self", sum2, 0, []uintptr{42}, 42},
29+ {"multiple args", sum4, 1, []uintptr{2, 3, 4}, 10},
30+ {"no variadic args", triple, 7, nil, 21},
31+ }
32+ for _, tc := range tests {
33+ t.Run(tc.name, func(t *testing.T) {
34+ if r1, _, _ := purego.SyscallSelf(tc.fn, tc.self, tc.args...); r1 != tc.want {
35+ t.Errorf("SyscallSelf returned %d, want %d", r1, tc.want)
36+ }
37+ })
38+ }
39+}
40+
41+func TestSyscallSelfPanics(t *testing.T) {
42+ t.Run("nil fn", func(t *testing.T) {
43+ defer func() {
44+ if recover() == nil {
45+ t.Error("expected panic for nil fn")
46+ }
47+ }()
48+ purego.SyscallSelf(0, 1)
49+ })
50+
51+ t.Run("too many args", func(t *testing.T) {
52+ defer func() {
53+ if got := fmt.Sprint(recover()); got != "purego: too many arguments to SyscallSelf" {
54+ t.Errorf("unexpected panic: %s", got)
55+ }
56+ }()
57+ fn := purego.NewCallback(func(uintptr) uintptr { return 0 })
58+ purego.SyscallSelf(fn, 1, make([]uintptr, purego.MaxArgs)...)
59+ })
60+}
61+
62+func TestSyscallSelfAllocs(t *testing.T) {
63+ fn := purego.NewCallback(func(self, a1, a2 uintptr) uintptr { return self + a1 + a2 })
64+ directAllocs := testing.AllocsPerRun(100, func() { purego.SyscallN(fn, 1, 2, 3) })
65+ selfAllocs := testing.AllocsPerRun(100, func() { purego.SyscallSelf(fn, 1, 2, 3) })
66+ if selfAllocs > directAllocs {
67+ t.Fatalf("SyscallSelf allocs = %v, SyscallN allocs = %v", selfAllocs, directAllocs)
68+ }
69+}
@@ -0,0 +1,69 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd || windows
5+
6+package purego_test
7+
8+import (
9+ "fmt"
10+ "testing"
11+
12+ "github.com/bnema/purego"
13+)
14+
15+func TestSyscallSelf(t *testing.T) {
16+ sum2 := purego.NewCallback(func(self, a1 uintptr) uintptr { return self + a1 })
17+ sum4 := purego.NewCallback(func(self, a1, a2, a3 uintptr) uintptr { return self + a1 + a2 + a3 })
18+ triple := purego.NewCallback(func(self uintptr) uintptr { return self * 3 })
19+
20+ tests := []struct {
21+ name string
22+ fn uintptr
23+ self uintptr
24+ args []uintptr
25+ want uintptr
26+ }{
27+ {"one arg", sum2, 10, []uintptr{20}, 30},
28+ {"zero self", sum2, 0, []uintptr{42}, 42},
29+ {"multiple args", sum4, 1, []uintptr{2, 3, 4}, 10},
30+ {"no variadic args", triple, 7, nil, 21},
31+ }
32+ for _, tc := range tests {
33+ t.Run(tc.name, func(t *testing.T) {
34+ if r1, _, _ := purego.SyscallSelf(tc.fn, tc.self, tc.args...); r1 != tc.want {
35+ t.Errorf("SyscallSelf returned %d, want %d", r1, tc.want)
36+ }
37+ })
38+ }
39+}
40+
41+func TestSyscallSelfPanics(t *testing.T) {
42+ t.Run("nil fn", func(t *testing.T) {
43+ defer func() {
44+ if recover() == nil {
45+ t.Error("expected panic for nil fn")
46+ }
47+ }()
48+ purego.SyscallSelf(0, 1)
49+ })
50+
51+ t.Run("too many args", func(t *testing.T) {
52+ defer func() {
53+ if got := fmt.Sprint(recover()); got != "purego: too many arguments to SyscallSelf" {
54+ t.Errorf("unexpected panic: %s", got)
55+ }
56+ }()
57+ fn := purego.NewCallback(func(uintptr) uintptr { return 0 })
58+ purego.SyscallSelf(fn, 1, make([]uintptr, purego.MaxArgs)...)
59+ })
60+}
61+
62+func TestSyscallSelfAllocs(t *testing.T) {
63+ fn := purego.NewCallback(func(self, a1, a2 uintptr) uintptr { return self + a1 + a2 })
64+ directAllocs := testing.AllocsPerRun(100, func() { purego.SyscallN(fn, 1, 2, 3) })
65+ selfAllocs := testing.AllocsPerRun(100, func() { purego.SyscallSelf(fn, 1, 2, 3) })
66+ if selfAllocs > directAllocs {
67+ t.Fatalf("SyscallSelf allocs = %v, SyscallN allocs = %v", selfAllocs, directAllocs)
68+ }
69+}

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