Merge ba8f819b4b7544f85ca9240baa1a59b34b0c8e87 into 9fea51b06733dd19f9573dcb56c9983948b45263
3 files · +98 -8
M .gitignore +1 -0
| @@ -1,1 +1,2 @@ | |||
| 1 | 1 | *~ | |
| 2 | + | .worktrees/ | |
| @@ -1,1 +1,2 @@ | |||||
| 1 | *~ | 1 | *~ | ||
| 2 | + | .worktrees/ | |||
M syscall_fixed.go +8 -8
| @@ -10,10 +10,6 @@ import ( | |||
| 10 | 10 | "unsafe" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | - | //go:noescape | |
| 14 | - | //go:linkname noescapeFixed runtime.noescape | |
| 15 | - | func noescapeFixed(p unsafe.Pointer) unsafe.Pointer | |
| 16 | - | ||
| 17 | 13 | // Syscall15 calls fn with up to 15 integer-class arguments without | |
| 18 | 14 | // allocating. Unused arguments must be 0. Unlike SyscallN it does not use a | |
| 19 | 15 | // variadic argument slice. Like SyscallN, a uintptr converted from an | |
| @@ -30,14 +26,18 @@ func Syscall15(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, | |||
| 30 | 26 | if runtime.GOOS == "windows" { | |
| 31 | 27 | return syscall_syscallN(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) | |
| 32 | 28 | } | |
| 33 | - | s := syscallArgs{ | |
| 29 | + | // Must not live on the goroutine stack: a Go callback made by fn can move | |
| 30 | + | // that stack, and syscallX would store results through the stale address. | |
| 31 | + | s := thePool.Get().(*syscallArgs) | |
| 32 | + | *s = syscallArgs{ | |
| 34 | 33 | fn: fn, a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, | |
| 35 | 34 | a6: a6, a7: a7, a8: a8, a9: a9, a10: a10, | |
| 36 | 35 | a11: a11, a12: a12, a13: a13, a14: a14, a15: a15, | |
| 37 | 36 | } | |
| 38 | - | // cgocall is synchronous: s remains on this goroutine's stack until it returns. | |
| 39 | - | runtime_cgocall(syscallXABI0, noescapeFixed(unsafe.Pointer(&s))) | |
| 40 | - | return s.a1, s.a2, s.a3 | |
| 37 | + | runtime_cgocall(syscallXABI0, unsafe.Pointer(s)) | |
| 38 | + | r1, r2, err = s.a1, s.a2, s.a3 | |
| 39 | + | thePool.Put(s) | |
| 40 | + | return r1, r2, err | |
| 41 | 41 | } | |
| 42 | 42 | ||
| 43 | 43 | // Syscall6 is Syscall15 with six arguments. Like Syscall15, pointer-derived | |
| @@ -10,10 +10,6 @@ import ( | |||||
| 10 | "unsafe" | 10 | "unsafe" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | - | //go:noescape | |||
| 14 | - | //go:linkname noescapeFixed runtime.noescape | |||
| 15 | - | func noescapeFixed(p unsafe.Pointer) unsafe.Pointer | |||
| 16 | - | ||||
| 17 | // Syscall15 calls fn with up to 15 integer-class arguments without | 13 | // Syscall15 calls fn with up to 15 integer-class arguments without | ||
| 18 | // allocating. Unused arguments must be 0. Unlike SyscallN it does not use a | 14 | // allocating. Unused arguments must be 0. Unlike SyscallN it does not use a | ||
| 19 | // variadic argument slice. Like SyscallN, a uintptr converted from an | 15 | // variadic argument slice. Like SyscallN, a uintptr converted from an | ||
| @@ -30,14 +26,18 @@ func Syscall15(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, | |||||
| 30 | if runtime.GOOS == "windows" { | 26 | if runtime.GOOS == "windows" { | ||
| 31 | return syscall_syscallN(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) | 27 | return syscall_syscallN(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) | ||
| 32 | } | 28 | } | ||
| 33 | - | s := syscallArgs{ | 29 | + | // Must not live on the goroutine stack: a Go callback made by fn can move |
| 30 | + | // that stack, and syscallX would store results through the stale address. | |||
| 31 | + | s := thePool.Get().(*syscallArgs) | |||
| 32 | + | *s = syscallArgs{ | |||
| 34 | fn: fn, a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, | 33 | fn: fn, a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, | ||
| 35 | a6: a6, a7: a7, a8: a8, a9: a9, a10: a10, | 34 | a6: a6, a7: a7, a8: a8, a9: a9, a10: a10, | ||
| 36 | a11: a11, a12: a12, a13: a13, a14: a14, a15: a15, | 35 | a11: a11, a12: a12, a13: a13, a14: a14, a15: a15, | ||
| 37 | } | 36 | } | ||
| 38 | - | // cgocall is synchronous: s remains on this goroutine's stack until it returns. | 37 | + | runtime_cgocall(syscallXABI0, unsafe.Pointer(s)) |
| 39 | - | runtime_cgocall(syscallXABI0, noescapeFixed(unsafe.Pointer(&s))) | 38 | + | r1, r2, err = s.a1, s.a2, s.a3 |
| 40 | - | return s.a1, s.a2, s.a3 | 39 | + | thePool.Put(s) |
| 40 | + | return r1, r2, err | |||
| 41 | } | 41 | } | ||
| 42 | 42 | ||||
| 43 | // Syscall6 is Syscall15 with six arguments. Like Syscall15, pointer-derived | 43 | // Syscall6 is Syscall15 with six arguments. Like Syscall15, pointer-derived | ||
A syscall_stack_test.go +89 -0
| @@ -0,0 +1,89 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2026 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | //go:build !windows && !plan9 | |
| 5 | + | ||
| 6 | + | package purego_test | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "runtime" | |
| 10 | + | "testing" | |
| 11 | + | "unsafe" | |
| 12 | + | ||
| 13 | + | "github.com/bnema/purego" | |
| 14 | + | "github.com/bnema/purego/internal/load" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | //go:noinline | |
| 18 | + | func growCallbackStack(depth int) { | |
| 19 | + | var pad [4096]byte | |
| 20 | + | pad[0] = byte(depth) | |
| 21 | + | if depth > 0 { | |
| 22 | + | growCallbackStack(depth - 1) | |
| 23 | + | } | |
| 24 | + | runtime.KeepAlive(pad) | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func TestSyscall6CallbackStackRelocation(t *testing.T) { | |
| 28 | + | library, err := getSystemLibrary() | |
| 29 | + | if err != nil { | |
| 30 | + | t.Fatal(err) | |
| 31 | + | } | |
| 32 | + | lib, err := load.OpenLibrary(library) | |
| 33 | + | if err != nil { | |
| 34 | + | t.Fatal(err) | |
| 35 | + | } | |
| 36 | + | defer load.CloseLibrary(lib) | |
| 37 | + | search, err := load.OpenSymbol(lib, "bsearch") | |
| 38 | + | if err != nil { | |
| 39 | + | t.Fatal(err) | |
| 40 | + | } | |
| 41 | + | for _, fast := range []bool{false, true} { | |
| 42 | + | name := "NewCallback" | |
| 43 | + | if fast { | |
| 44 | + | name = "NewCallbackInts" | |
| 45 | + | } | |
| 46 | + | t.Run(name, func(t *testing.T) { | |
| 47 | + | calls := 0 | |
| 48 | + | compare := func() uintptr { calls++; growCallbackStack(128); return 1 } | |
| 49 | + | var cb uintptr | |
| 50 | + | if fast { | |
| 51 | + | cb = purego.NewCallbackInts(func(*purego.CallbackArgs) uintptr { return compare() }) | |
| 52 | + | } else { | |
| 53 | + | cb = purego.NewCallback(func(_ purego.CDecl, _, _ uintptr) uintptr { return compare() }) | |
| 54 | + | } | |
| 55 | + | // A fresh goroutine starts with a small stack for each callback kind. | |
| 56 | + | done := make(chan uintptr, 1) | |
| 57 | + | go func() { | |
| 58 | + | key, data := new(uintptr), new(uintptr) | |
| 59 | + | r, _, _ := purego.Syscall6(search, uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(data)), 1, unsafe.Sizeof(*data), cb, 0) | |
| 60 | + | done <- r | |
| 61 | + | }() | |
| 62 | + | if got := <-done; got != 0 { | |
| 63 | + | t.Errorf("bsearch returned %#x, want NULL (key absent)", got) | |
| 64 | + | } | |
| 65 | + | if calls != 1 { | |
| 66 | + | t.Errorf("comparator called %d times, want 1", calls) | |
| 67 | + | } | |
| 68 | + | }) | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func TestSyscall6Allocs(t *testing.T) { | |
| 73 | + | library, err := getSystemLibrary() | |
| 74 | + | if err != nil { | |
| 75 | + | t.Fatal(err) | |
| 76 | + | } | |
| 77 | + | lib, err := load.OpenLibrary(library) | |
| 78 | + | if err != nil { | |
| 79 | + | t.Fatal(err) | |
| 80 | + | } | |
| 81 | + | defer load.CloseLibrary(lib) | |
| 82 | + | fn, err := load.OpenSymbol(lib, "abs") | |
| 83 | + | if err != nil { | |
| 84 | + | t.Fatal(err) | |
| 85 | + | } | |
| 86 | + | if got := testing.AllocsPerRun(100, func() { purego.Syscall6(fn, 1, 0, 0, 0, 0, 0) }); got != 0 { | |
| 87 | + | t.Fatalf("Syscall6 allocations = %v, want 0", got) | |
| 88 | + | } | |
| 89 | + | } | |
| @@ -0,0 +1,89 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2026 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | //go:build !windows && !plan9 | |||
| 5 | + | ||||
| 6 | + | package purego_test | |||
| 7 | + | ||||
| 8 | + | import ( | |||
| 9 | + | "runtime" | |||
| 10 | + | "testing" | |||
| 11 | + | "unsafe" | |||
| 12 | + | ||||
| 13 | + | "github.com/bnema/purego" | |||
| 14 | + | "github.com/bnema/purego/internal/load" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | //go:noinline | |||
| 18 | + | func growCallbackStack(depth int) { | |||
| 19 | + | var pad [4096]byte | |||
| 20 | + | pad[0] = byte(depth) | |||
| 21 | + | if depth > 0 { | |||
| 22 | + | growCallbackStack(depth - 1) | |||
| 23 | + | } | |||
| 24 | + | runtime.KeepAlive(pad) | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func TestSyscall6CallbackStackRelocation(t *testing.T) { | |||
| 28 | + | library, err := getSystemLibrary() | |||
| 29 | + | if err != nil { | |||
| 30 | + | t.Fatal(err) | |||
| 31 | + | } | |||
| 32 | + | lib, err := load.OpenLibrary(library) | |||
| 33 | + | if err != nil { | |||
| 34 | + | t.Fatal(err) | |||
| 35 | + | } | |||
| 36 | + | defer load.CloseLibrary(lib) | |||
| 37 | + | search, err := load.OpenSymbol(lib, "bsearch") | |||
| 38 | + | if err != nil { | |||
| 39 | + | t.Fatal(err) | |||
| 40 | + | } | |||
| 41 | + | for _, fast := range []bool{false, true} { | |||
| 42 | + | name := "NewCallback" | |||
| 43 | + | if fast { | |||
| 44 | + | name = "NewCallbackInts" | |||
| 45 | + | } | |||
| 46 | + | t.Run(name, func(t *testing.T) { | |||
| 47 | + | calls := 0 | |||
| 48 | + | compare := func() uintptr { calls++; growCallbackStack(128); return 1 } | |||
| 49 | + | var cb uintptr | |||
| 50 | + | if fast { | |||
| 51 | + | cb = purego.NewCallbackInts(func(*purego.CallbackArgs) uintptr { return compare() }) | |||
| 52 | + | } else { | |||
| 53 | + | cb = purego.NewCallback(func(_ purego.CDecl, _, _ uintptr) uintptr { return compare() }) | |||
| 54 | + | } | |||
| 55 | + | // A fresh goroutine starts with a small stack for each callback kind. | |||
| 56 | + | done := make(chan uintptr, 1) | |||
| 57 | + | go func() { | |||
| 58 | + | key, data := new(uintptr), new(uintptr) | |||
| 59 | + | r, _, _ := purego.Syscall6(search, uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(data)), 1, unsafe.Sizeof(*data), cb, 0) | |||
| 60 | + | done <- r | |||
| 61 | + | }() | |||
| 62 | + | if got := <-done; got != 0 { | |||
| 63 | + | t.Errorf("bsearch returned %#x, want NULL (key absent)", got) | |||
| 64 | + | } | |||
| 65 | + | if calls != 1 { | |||
| 66 | + | t.Errorf("comparator called %d times, want 1", calls) | |||
| 67 | + | } | |||
| 68 | + | }) | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func TestSyscall6Allocs(t *testing.T) { | |||
| 73 | + | library, err := getSystemLibrary() | |||
| 74 | + | if err != nil { | |||
| 75 | + | t.Fatal(err) | |||
| 76 | + | } | |||
| 77 | + | lib, err := load.OpenLibrary(library) | |||
| 78 | + | if err != nil { | |||
| 79 | + | t.Fatal(err) | |||
| 80 | + | } | |||
| 81 | + | defer load.CloseLibrary(lib) | |||
| 82 | + | fn, err := load.OpenSymbol(lib, "abs") | |||
| 83 | + | if err != nil { | |||
| 84 | + | t.Fatal(err) | |||
| 85 | + | } | |||
| 86 | + | if got := testing.AllocsPerRun(100, func() { purego.Syscall6(fn, 1, 0, 0, 0, 0, 0) }); got != 0 { | |||
| 87 | + | t.Fatalf("Syscall6 allocations = %v, want 0", got) | |||
| 88 | + | } | |||
| 89 | + | } | |||