purego: add SyscallSelf for allocation-free method calls
- commit
- c0b72ddfe6853f0d75aa30547052a9ca44a3eb5c
- author
- brice · · 2026-09-27 05:54 UTC
- parent
- 5509d1a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 79 | 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 | 90 | // Windows uses syscall.SyscallN in syscall_windows.go. | |
| 83 | 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 | 94 | var floats [maxArgs]uintptr | |
| 91 | 95 | copy(floats[:], tmp[:]) | |
| 92 | 96 | 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]uintptr | 94 | 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) { | |||
| 81 | 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 | 92 | // Windows uses syscall.SyscallN in syscall_windows.go. | |
| 85 | 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 | 96 | var floats [16]uintptr | |
| 93 | 97 | copy(floats[:], tmp[:16]) | |
| 94 | 98 | 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]uintptr | 96 | 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) { | |||
| 63 | 63 | panic("purego: too many arguments to SyscallN") | |
| 64 | 64 | } | |
| 65 | 65 | ||
| 66 | + | // Add padding so there is no out-of-bounds slicing. | |
| 66 | 67 | 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 | 74 | var floats [maxArgs]uintptr | |
| 69 | 75 | copy(floats[:], tmp[:]) | |
| 70 | 76 | 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]uintptr | 67 | 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]uintptr | 74 | 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 | + | } | |||