purego: Windows support for RegisterLibFunc and RegisterFunc (#131)
* purego: Windows support for RegisterLibFunc and RegisterFunc It implements these two functions to make the code between the two more similiar. It also updates the libc example to work on Windows. This PR does not add support for Windows 386 or 32bit arm. Closes #130
- commit
- d888bce98506955251c6b11981556f2cf8ac2ff0
- author
- TotallyGamerJet · · 2023-04-15 14:55 UTC
- committer
- GitHub · · 2023-04-15 14:55 UTC
- parent
- ec16513
- signature
- GPG Unverified
4AEE18F83AFDEB23The key is unknown - refs
- tag: v0.4.0-alpha.4
15 files · +287 -174
M .github/workflows/test.yml +2 -0
| @@ -76,6 +76,8 @@ jobs: | |||
| 76 | 76 | env CGO_ENABLED=0 go test -shuffle=on -v -count=10 ./... | |
| 77 | 77 | env CGO_ENABLED=1 go test -shuffle=on -v -count=10 ./... | |
| 78 | 78 | ||
| 79 | + | # TODO: add Windows 386 tests | |
| 80 | + | ||
| 79 | 81 | - name: go test race | |
| 80 | 82 | if: ${{ !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') }} | |
| 81 | 83 | run: | | |
| @@ -76,6 +76,8 @@ jobs: | |||||
| 76 | env CGO_ENABLED=0 go test -shuffle=on -v -count=10 ./... | 76 | env CGO_ENABLED=0 go test -shuffle=on -v -count=10 ./... | ||
| 77 | env CGO_ENABLED=1 go test -shuffle=on -v -count=10 ./... | 77 | env CGO_ENABLED=1 go test -shuffle=on -v -count=10 ./... | ||
| 78 | 78 | ||||
| 79 | + | # TODO: add Windows 386 tests | |||
| 80 | + | ||||
| 79 | - name: go test race | 81 | - name: go test race | ||
| 80 | if: ${{ !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') }} | 82 | if: ${{ !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') }} | ||
| 81 | run: | | 83 | run: | | ||
M README.md +2 -0
| @@ -19,6 +19,8 @@ born to bring that same vision to the other platforms supported by Ebitengine. | |||
| 19 | 19 | ||
| 20 | 20 | ## Example | |
| 21 | 21 | ||
| 22 | + | This example only works on macOS and Linux. For a complete example look at [libc](https://github.com/ebitengine/purego/tree/main/examples/libc) which supports Windows. | |
| 23 | + | ||
| 22 | 24 | ```go | |
| 23 | 25 | package main | |
| 24 | 26 | ||
| @@ -19,6 +19,8 @@ born to bring that same vision to the other platforms supported by Ebitengine. | |||||
| 19 | 19 | ||||
| 20 | ## Example | 20 | ## Example | ||
| 21 | 21 | ||||
| 22 | + | This example only works on macOS and Linux. For a complete example look at [libc](https://github.com/ebitengine/purego/tree/main/examples/libc) which supports Windows. | |||
| 23 | + | ||||
| 22 | ```go | 24 | ```go | ||
| 23 | package main | 25 | package main | ||
| 24 | 26 | ||||
M dlfcn.go +9 -0
| @@ -63,6 +63,15 @@ func Dlclose(handle uintptr) error { | |||
| 63 | 63 | return nil | |
| 64 | 64 | } | |
| 65 | 65 | ||
| 66 | + | //go:linkname openLibrary openLibrary | |
| 67 | + | func openLibrary(name string) (uintptr, error) { | |
| 68 | + | return Dlopen(name, RTLD_NOW|RTLD_GLOBAL) | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func loadSymbol(handle uintptr, name string) (uintptr, error) { | |
| 72 | + | return Dlsym(handle, name) | |
| 73 | + | } | |
| 74 | + | ||
| 66 | 75 | // these functions exist in dlfcn_stubs.s and are calling C functions linked to in dlfcn_GOOS.go | |
| 67 | 76 | // the indirection is necessary because a function is actually a pointer to the pointer to the code. | |
| 68 | 77 | // sadly, I do not know of anyway to remove the assembly stubs entirely because //go:linkname doesn't | |
| @@ -63,6 +63,15 @@ func Dlclose(handle uintptr) error { | |||||
| 63 | return nil | 63 | return nil | ||
| 64 | } | 64 | } | ||
| 65 | 65 | ||||
| 66 | + | //go:linkname openLibrary openLibrary | |||
| 67 | + | func openLibrary(name string) (uintptr, error) { | |||
| 68 | + | return Dlopen(name, RTLD_NOW|RTLD_GLOBAL) | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | func loadSymbol(handle uintptr, name string) (uintptr, error) { | |||
| 72 | + | return Dlsym(handle, name) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 66 | // these functions exist in dlfcn_stubs.s and are calling C functions linked to in dlfcn_GOOS.go | 75 | // these functions exist in dlfcn_stubs.s and are calling C functions linked to in dlfcn_GOOS.go | ||
| 67 | // the indirection is necessary because a function is actually a pointer to the pointer to the code. | 76 | // the indirection is necessary because a function is actually a pointer to the pointer to the code. | ||
| 68 | // sadly, I do not know of anyway to remove the assembly stubs entirely because //go:linkname doesn't | 77 | // sadly, I do not know of anyway to remove the assembly stubs entirely because //go:linkname doesn't | ||
M dlfcn_test.go +0 -78
| @@ -14,88 +14,10 @@ import ( | |||
| 14 | 14 | "runtime" | |
| 15 | 15 | "strings" | |
| 16 | 16 | "testing" | |
| 17 | - | "unsafe" | |
| 18 | 17 | ||
| 19 | 18 | "github.com/ebitengine/purego" | |
| 20 | 19 | ) | |
| 21 | 20 | ||
| 22 | - | func getSystemLibrary() (string, error) { | |
| 23 | - | switch runtime.GOOS { | |
| 24 | - | case "darwin": | |
| 25 | - | return "/usr/lib/libSystem.B.dylib", nil | |
| 26 | - | case "linux": | |
| 27 | - | return "libc.so.6", nil | |
| 28 | - | default: | |
| 29 | - | return "", fmt.Errorf("GOOS=%s is not supported", runtime.GOOS) | |
| 30 | - | } | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | func TestRegisterFunc(t *testing.T) { | |
| 34 | - | library, err := getSystemLibrary() | |
| 35 | - | if err != nil { | |
| 36 | - | t.Errorf("couldn't get system library: %s", err) | |
| 37 | - | } | |
| 38 | - | libc, err := purego.Dlopen(library, purego.RTLD_NOW|purego.RTLD_GLOBAL) | |
| 39 | - | if err != nil { | |
| 40 | - | t.Errorf("failed to dlopen: %s", err) | |
| 41 | - | } | |
| 42 | - | var puts func(string) | |
| 43 | - | purego.RegisterLibFunc(&puts, libc, "puts") | |
| 44 | - | puts("Calling C from from Go without Cgo!") | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | func ExampleNewCallback() { | |
| 48 | - | if runtime.GOOS == "linux" { | |
| 49 | - | // TODO: enable once callbacks are working properly on Linux | |
| 50 | - | fmt.Println("1 2 3 4 5 6 7 8 9\n45") | |
| 51 | - | return | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | cb := purego.NewCallback(func(a1, a2, a3, a4, a5, a6, a7, a8, a9 int) int { | |
| 55 | - | fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9) | |
| 56 | - | return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 | |
| 57 | - | }) | |
| 58 | - | ||
| 59 | - | var fn func(a1, a2, a3, a4, a5, a6, a7, a8, a9 int) int | |
| 60 | - | purego.RegisterFunc(&fn, cb) | |
| 61 | - | ||
| 62 | - | ret := fn(1, 2, 3, 4, 5, 6, 7, 8, 9) | |
| 63 | - | fmt.Println(ret) | |
| 64 | - | ||
| 65 | - | //Output: 1 2 3 4 5 6 7 8 9 | |
| 66 | - | // 45 | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | func Test_qsort(t *testing.T) { | |
| 70 | - | if runtime.GOOS == "linux" { | |
| 71 | - | // TODO: enable once callbacks are working properly on Linux | |
| 72 | - | t.SkipNow() | |
| 73 | - | } | |
| 74 | - | ||
| 75 | - | library, err := getSystemLibrary() | |
| 76 | - | if err != nil { | |
| 77 | - | t.Errorf("couldn't get system library: %s", err) | |
| 78 | - | } | |
| 79 | - | libc, err := purego.Dlopen(library, purego.RTLD_NOW|purego.RTLD_GLOBAL) | |
| 80 | - | if err != nil { | |
| 81 | - | t.Errorf("failed to dlopen: %s", err) | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | var data = []int{88, 56, 100, 2, 25} | |
| 85 | - | var sorted = []int{2, 25, 56, 88, 100} | |
| 86 | - | compare := func(a, b *int) int { | |
| 87 | - | return *a - *b | |
| 88 | - | } | |
| 89 | - | var qsort func(data []int, nitms uintptr, size uintptr, compar func(a, b *int) int) | |
| 90 | - | purego.RegisterLibFunc(&qsort, libc, "qsort") | |
| 91 | - | qsort(data, uintptr(len(data)), unsafe.Sizeof(int(0)), compare) | |
| 92 | - | for i := range data { | |
| 93 | - | if data[i] != sorted[i] { | |
| 94 | - | t.Errorf("got %d wanted %d at %d", data[i], sorted[i], i) | |
| 95 | - | } | |
| 96 | - | } | |
| 97 | - | } | |
| 98 | - | ||
| 99 | 21 | func TestNestedDlopenCall(t *testing.T) { | |
| 100 | 22 | libFileName := filepath.Join(t.TempDir(), "libdlnested.so") | |
| 101 | 23 | t.Logf("Build %v", libFileName) | |
| @@ -14,88 +14,10 @@ import ( | |||||
| 14 | "runtime" | 14 | "runtime" | ||
| 15 | "strings" | 15 | "strings" | ||
| 16 | "testing" | 16 | "testing" | ||
| 17 | - | "unsafe" | |||
| 18 | 17 | ||||
| 19 | "github.com/ebitengine/purego" | 18 | "github.com/ebitengine/purego" | ||
| 20 | ) | 19 | ) | ||
| 21 | 20 | ||||
| 22 | - | func getSystemLibrary() (string, error) { | |||
| 23 | - | switch runtime.GOOS { | |||
| 24 | - | case "darwin": | |||
| 25 | - | return "/usr/lib/libSystem.B.dylib", nil | |||
| 26 | - | case "linux": | |||
| 27 | - | return "libc.so.6", nil | |||
| 28 | - | default: | |||
| 29 | - | return "", fmt.Errorf("GOOS=%s is not supported", runtime.GOOS) | |||
| 30 | - | } | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | - | func TestRegisterFunc(t *testing.T) { | |||
| 34 | - | library, err := getSystemLibrary() | |||
| 35 | - | if err != nil { | |||
| 36 | - | t.Errorf("couldn't get system library: %s", err) | |||
| 37 | - | } | |||
| 38 | - | libc, err := purego.Dlopen(library, purego.RTLD_NOW|purego.RTLD_GLOBAL) | |||
| 39 | - | if err != nil { | |||
| 40 | - | t.Errorf("failed to dlopen: %s", err) | |||
| 41 | - | } | |||
| 42 | - | var puts func(string) | |||
| 43 | - | purego.RegisterLibFunc(&puts, libc, "puts") | |||
| 44 | - | puts("Calling C from from Go without Cgo!") | |||
| 45 | - | } | |||
| 46 | - | ||||
| 47 | - | func ExampleNewCallback() { | |||
| 48 | - | if runtime.GOOS == "linux" { | |||
| 49 | - | // TODO: enable once callbacks are working properly on Linux | |||
| 50 | - | fmt.Println("1 2 3 4 5 6 7 8 9\n45") | |||
| 51 | - | return | |||
| 52 | - | } | |||
| 53 | - | ||||
| 54 | - | cb := purego.NewCallback(func(a1, a2, a3, a4, a5, a6, a7, a8, a9 int) int { | |||
| 55 | - | fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9) | |||
| 56 | - | return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 | |||
| 57 | - | }) | |||
| 58 | - | ||||
| 59 | - | var fn func(a1, a2, a3, a4, a5, a6, a7, a8, a9 int) int | |||
| 60 | - | purego.RegisterFunc(&fn, cb) | |||
| 61 | - | ||||
| 62 | - | ret := fn(1, 2, 3, 4, 5, 6, 7, 8, 9) | |||
| 63 | - | fmt.Println(ret) | |||
| 64 | - | ||||
| 65 | - | //Output: 1 2 3 4 5 6 7 8 9 | |||
| 66 | - | // 45 | |||
| 67 | - | } | |||
| 68 | - | ||||
| 69 | - | func Test_qsort(t *testing.T) { | |||
| 70 | - | if runtime.GOOS == "linux" { | |||
| 71 | - | // TODO: enable once callbacks are working properly on Linux | |||
| 72 | - | t.SkipNow() | |||
| 73 | - | } | |||
| 74 | - | ||||
| 75 | - | library, err := getSystemLibrary() | |||
| 76 | - | if err != nil { | |||
| 77 | - | t.Errorf("couldn't get system library: %s", err) | |||
| 78 | - | } | |||
| 79 | - | libc, err := purego.Dlopen(library, purego.RTLD_NOW|purego.RTLD_GLOBAL) | |||
| 80 | - | if err != nil { | |||
| 81 | - | t.Errorf("failed to dlopen: %s", err) | |||
| 82 | - | } | |||
| 83 | - | ||||
| 84 | - | var data = []int{88, 56, 100, 2, 25} | |||
| 85 | - | var sorted = []int{2, 25, 56, 88, 100} | |||
| 86 | - | compare := func(a, b *int) int { | |||
| 87 | - | return *a - *b | |||
| 88 | - | } | |||
| 89 | - | var qsort func(data []int, nitms uintptr, size uintptr, compar func(a, b *int) int) | |||
| 90 | - | purego.RegisterLibFunc(&qsort, libc, "qsort") | |||
| 91 | - | qsort(data, uintptr(len(data)), unsafe.Sizeof(int(0)), compare) | |||
| 92 | - | for i := range data { | |||
| 93 | - | if data[i] != sorted[i] { | |||
| 94 | - | t.Errorf("got %d wanted %d at %d", data[i], sorted[i], i) | |||
| 95 | - | } | |||
| 96 | - | } | |||
| 97 | - | } | |||
| 98 | - | ||||
| 99 | func TestNestedDlopenCall(t *testing.T) { | 21 | func TestNestedDlopenCall(t *testing.T) { | ||
| 100 | libFileName := filepath.Join(t.TempDir(), "libdlnested.so") | 22 | libFileName := filepath.Join(t.TempDir(), "libdlnested.so") | ||
| 101 | t.Logf("Build %v", libFileName) | 23 | t.Logf("Build %v", libFileName) | ||
M examples/libc/main.go +4 -2
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | |
| 3 | 3 | ||
| 4 | - | //go:build darwin || linux | |
| 4 | + | //go:build darwin || linux || windows | |
| 5 | 5 | ||
| 6 | 6 | package main | |
| 7 | 7 | ||
| @@ -18,13 +18,15 @@ func getSystemLibrary() string { | |||
| 18 | 18 | return "/usr/lib/libSystem.B.dylib" | |
| 19 | 19 | case "linux": | |
| 20 | 20 | return "libc.so.6" | |
| 21 | + | case "windows": | |
| 22 | + | return "ucrtbase.dll" | |
| 21 | 23 | default: | |
| 22 | 24 | panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)) | |
| 23 | 25 | } | |
| 24 | 26 | } | |
| 25 | 27 | ||
| 26 | 28 | func main() { | |
| 27 | - | libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL) | |
| 29 | + | libc, err := openLibrary(getSystemLibrary()) | |
| 28 | 30 | if err != nil { | |
| 29 | 31 | panic(err) | |
| 30 | 32 | } | |
| @@ -1,7 +1,7 @@ | |||||
| 1 | // SPDX-License-Identifier: Apache-2.0 | 1 | // SPDX-License-Identifier: Apache-2.0 | ||
| 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | ||
| 3 | 3 | ||||
| 4 | - | //go:build darwin || linux | 4 | + | //go:build darwin || linux || windows |
| 5 | 5 | ||||
| 6 | package main | 6 | package main | ||
| 7 | 7 | ||||
| @@ -18,13 +18,15 @@ func getSystemLibrary() string { | |||||
| 18 | return "/usr/lib/libSystem.B.dylib" | 18 | return "/usr/lib/libSystem.B.dylib" | ||
| 19 | case "linux": | 19 | case "linux": | ||
| 20 | return "libc.so.6" | 20 | return "libc.so.6" | ||
| 21 | + | case "windows": | |||
| 22 | + | return "ucrtbase.dll" | |||
| 21 | default: | 23 | default: | ||
| 22 | panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)) | 24 | panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)) | ||
| 23 | } | 25 | } | ||
| 24 | } | 26 | } | ||
| 25 | 27 | ||||
| 26 | func main() { | 28 | func main() { | ||
| 27 | - | libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL) | 29 | + | libc, err := openLibrary(getSystemLibrary()) |
| 28 | if err != nil { | 30 | if err != nil { | ||
| 29 | panic(err) | 31 | panic(err) | ||
| 30 | } | 32 | } | ||
A examples/libc/main_unix.go +12 -0
| @@ -0,0 +1,12 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2023 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | //go:build darwin || linux | |
| 5 | + | ||
| 6 | + | package main | |
| 7 | + | ||
| 8 | + | import "github.com/ebitengine/purego" | |
| 9 | + | ||
| 10 | + | func openLibrary(name string) (uintptr, error) { | |
| 11 | + | return purego.Dlopen(name, purego.RTLD_NOW|purego.RTLD_GLOBAL) | |
| 12 | + | } | |
| @@ -0,0 +1,12 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2023 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | //go:build darwin || linux | |||
| 5 | + | ||||
| 6 | + | package main | |||
| 7 | + | ||||
| 8 | + | import "github.com/ebitengine/purego" | |||
| 9 | + | ||||
| 10 | + | func openLibrary(name string) (uintptr, error) { | |||
| 11 | + | return purego.Dlopen(name, purego.RTLD_NOW|purego.RTLD_GLOBAL) | |||
| 12 | + | } | |||
A examples/libc/main_windows.go +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2023 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | package main | |
| 5 | + | ||
| 6 | + | import "golang.org/x/sys/windows" | |
| 7 | + | ||
| 8 | + | func openLibrary(name string) (uintptr, error) { | |
| 9 | + | handle, err := windows.LoadLibrary(name) | |
| 10 | + | return uintptr(handle), err | |
| 11 | + | } | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2023 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | package main | |||
| 5 | + | ||||
| 6 | + | import "golang.org/x/sys/windows" | |||
| 7 | + | ||||
| 8 | + | func openLibrary(name string) (uintptr, error) { | |||
| 9 | + | handle, err := windows.LoadLibrary(name) | |||
| 10 | + | return uintptr(handle), err | |||
| 11 | + | } | |||
M func.go +59 -37
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | |
| 3 | 3 | ||
| 4 | - | //go:build darwin || linux | |
| 4 | + | //go:build darwin || linux || windows | |
| 5 | 5 | ||
| 6 | 6 | package purego | |
| 7 | 7 | ||
| @@ -15,11 +15,9 @@ import ( | |||
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | 17 | // RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name). | |
| 18 | - | // It panics if Dlsym fails. | |
| 19 | - | // | |
| 20 | - | // Windows does not support this function. | |
| 18 | + | // It panics if it can't find the name symbol. | |
| 21 | 19 | func RegisterLibFunc(fptr interface{}, handle uintptr, name string) { | |
| 22 | - | sym, err := Dlsym(handle, name) | |
| 20 | + | sym, err := loadSymbol(handle, name) | |
| 23 | 21 | if err != nil { | |
| 24 | 22 | panic(err) | |
| 25 | 23 | } | |
| @@ -67,8 +65,6 @@ func RegisterLibFunc(fptr interface{}, handle uintptr, name string) { | |||
| 67 | 65 | // There are some limitations when using RegisterFunc on Linux. First, there is no support for function arguments. | |
| 68 | 66 | // Second, float32 and float64 arguments and return values do not work when CGO_ENABLED=1. Otherwise, Linux | |
| 69 | 67 | // has the same feature parity as Darwin. | |
| 70 | - | // | |
| 71 | - | // Windows does not support this function. | |
| 72 | 68 | func RegisterFunc(fptr interface{}, cfn uintptr) { | |
| 73 | 69 | fn := reflect.ValueOf(fptr).Elem() | |
| 74 | 70 | ty := fn.Type() | |
| @@ -132,18 +128,43 @@ func RegisterFunc(fptr interface{}, cfn uintptr) { | |||
| 132 | 128 | var numInts int | |
| 133 | 129 | var numFloats int | |
| 134 | 130 | var numStack int | |
| 135 | - | addStack := func(x uintptr) { | |
| 136 | - | stack[numStack] = x | |
| 137 | - | numStack++ | |
| 138 | - | } | |
| 139 | - | addInt := func(x uintptr) { | |
| 140 | - | if numInts >= numOfIntegerRegisters() { | |
| 141 | - | addStack(x) | |
| 142 | - | } else { | |
| 143 | - | sysargs[numInts] = x | |
| 144 | - | numInts++ | |
| 131 | + | var addStack, addInt, addFloat func(x uintptr) | |
| 132 | + | if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { | |
| 133 | + | // Windows arm64 uses the same calling convention as macOS and Linux | |
| 134 | + | addStack = func(x uintptr) { | |
| 135 | + | stack[numStack] = x | |
| 136 | + | numStack++ | |
| 137 | + | } | |
| 138 | + | addInt = func(x uintptr) { | |
| 139 | + | if numInts >= numOfIntegerRegisters() { | |
| 140 | + | addStack(x) | |
| 141 | + | } else { | |
| 142 | + | sysargs[numInts] = x | |
| 143 | + | numInts++ | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | addFloat = func(x uintptr) { | |
| 147 | + | if numFloats < len(floats) { | |
| 148 | + | floats[numFloats] = x | |
| 149 | + | numFloats++ | |
| 150 | + | } else { | |
| 151 | + | addStack(x) | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | } else { | |
| 155 | + | // On Windows amd64 the arguments are passed in the numbered registered. | |
| 156 | + | // So the first int is in the first integer register and the first float | |
| 157 | + | // is in the second floating register if there is already a first int. | |
| 158 | + | // This is in contrast to how macOS and Linux pass arguments which | |
| 159 | + | // tries to use as many registers as possible in the calling convention. | |
| 160 | + | addStack = func(x uintptr) { | |
| 161 | + | sysargs[numStack] = x | |
| 162 | + | numStack++ | |
| 145 | 163 | } | |
| 164 | + | addInt = addStack | |
| 165 | + | addFloat = addStack | |
| 146 | 166 | } | |
| 167 | + | ||
| 147 | 168 | var keepAlive []interface{} | |
| 148 | 169 | defer func() { | |
| 149 | 170 | runtime.KeepAlive(keepAlive) | |
| @@ -171,33 +192,29 @@ func RegisterFunc(fptr interface{}, cfn uintptr) { | |||
| 171 | 192 | addInt(0) | |
| 172 | 193 | } | |
| 173 | 194 | case reflect.Float32: | |
| 174 | - | if numFloats < len(floats) { | |
| 175 | - | floats[numFloats] = uintptr(math.Float32bits(float32(v.Float()))) | |
| 176 | - | numFloats++ | |
| 177 | - | } else { | |
| 178 | - | addStack(uintptr(math.Float32bits(float32(v.Float())))) | |
| 179 | - | } | |
| 195 | + | addFloat(uintptr(math.Float32bits(float32(v.Float())))) | |
| 180 | 196 | case reflect.Float64: | |
| 181 | - | if numFloats < len(floats) { | |
| 182 | - | floats[numFloats] = uintptr(math.Float64bits(v.Float())) | |
| 183 | - | numFloats++ | |
| 184 | - | } else { | |
| 185 | - | addStack(uintptr(math.Float64bits(v.Float()))) | |
| 186 | - | } | |
| 197 | + | addFloat(uintptr(math.Float64bits(v.Float()))) | |
| 187 | 198 | default: | |
| 188 | 199 | panic("purego: unsupported kind: " + v.Kind().String()) | |
| 189 | 200 | } | |
| 190 | 201 | } | |
| 191 | 202 | // TODO: support structs | |
| 192 | - | syscall := syscall9Args{ | |
| 193 | - | cfn, | |
| 194 | - | sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5], sysargs[6], sysargs[7], sysargs[8], | |
| 195 | - | floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7], | |
| 196 | - | 0, 0, 0, | |
| 203 | + | var r1, r2 uintptr | |
| 204 | + | if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { | |
| 205 | + | // Use the normal arm64 calling convention even on Windows | |
| 206 | + | syscall := syscall9Args{ | |
| 207 | + | cfn, | |
| 208 | + | sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5], sysargs[6], sysargs[7], sysargs[8], | |
| 209 | + | floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7], | |
| 210 | + | 0, 0, 0, | |
| 211 | + | } | |
| 212 | + | runtime_cgocall(syscall9XABI0, unsafe.Pointer(&syscall)) | |
| 213 | + | r1, r2 = syscall.r1, syscall.r2 | |
| 214 | + | } else { | |
| 215 | + | // This is a fallback for amd64, 386, and arm. Note this may not support floats | |
| 216 | + | r1, r2, _ = syscall_syscall9X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5], sysargs[6], sysargs[7], sysargs[8]) | |
| 197 | 217 | } | |
| 198 | - | runtime_cgocall(syscall9XABI0, unsafe.Pointer(&syscall)) | |
| 199 | - | r1, r2 := syscall.r1, syscall.r2 | |
| 200 | - | ||
| 201 | 218 | if ty.NumOut() == 0 { | |
| 202 | 219 | return nil | |
| 203 | 220 | } | |
| @@ -240,6 +257,11 @@ func numOfIntegerRegisters() int { | |||
| 240 | 257 | return 8 | |
| 241 | 258 | case "amd64": | |
| 242 | 259 | return 6 | |
| 260 | + | // TODO: figure out why 386 tests are not working | |
| 261 | + | /*case "386": | |
| 262 | + | return 0 | |
| 263 | + | case "arm": | |
| 264 | + | return 4*/ | |
| 243 | 265 | default: | |
| 244 | 266 | panic("purego: unknown GOARCH (" + runtime.GOARCH + ")") | |
| 245 | 267 | } | |
| @@ -1,7 +1,7 @@ | |||||
| 1 | // SPDX-License-Identifier: Apache-2.0 | 1 | // SPDX-License-Identifier: Apache-2.0 | ||
| 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | ||
| 3 | 3 | ||||
| 4 | - | //go:build darwin || linux | 4 | + | //go:build darwin || linux || windows |
| 5 | 5 | ||||
| 6 | package purego | 6 | package purego | ||
| 7 | 7 | ||||
| @@ -15,11 +15,9 @@ import ( | |||||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | // RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name). | 17 | // RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name). | ||
| 18 | - | // It panics if Dlsym fails. | 18 | + | // It panics if it can't find the name symbol. |
| 19 | - | // | |||
| 20 | - | // Windows does not support this function. | |||
| 21 | func RegisterLibFunc(fptr interface{}, handle uintptr, name string) { | 19 | func RegisterLibFunc(fptr interface{}, handle uintptr, name string) { | ||
| 22 | - | sym, err := Dlsym(handle, name) | 20 | + | sym, err := loadSymbol(handle, name) |
| 23 | if err != nil { | 21 | if err != nil { | ||
| 24 | panic(err) | 22 | panic(err) | ||
| 25 | } | 23 | } | ||
| @@ -67,8 +65,6 @@ func RegisterLibFunc(fptr interface{}, handle uintptr, name string) { | |||||
| 67 | // There are some limitations when using RegisterFunc on Linux. First, there is no support for function arguments. | 65 | // There are some limitations when using RegisterFunc on Linux. First, there is no support for function arguments. | ||
| 68 | // Second, float32 and float64 arguments and return values do not work when CGO_ENABLED=1. Otherwise, Linux | 66 | // Second, float32 and float64 arguments and return values do not work when CGO_ENABLED=1. Otherwise, Linux | ||
| 69 | // has the same feature parity as Darwin. | 67 | // has the same feature parity as Darwin. | ||
| 70 | - | // | |||
| 71 | - | // Windows does not support this function. | |||
| 72 | func RegisterFunc(fptr interface{}, cfn uintptr) { | 68 | func RegisterFunc(fptr interface{}, cfn uintptr) { | ||
| 73 | fn := reflect.ValueOf(fptr).Elem() | 69 | fn := reflect.ValueOf(fptr).Elem() | ||
| 74 | ty := fn.Type() | 70 | ty := fn.Type() | ||
| @@ -132,18 +128,43 @@ func RegisterFunc(fptr interface{}, cfn uintptr) { | |||||
| 132 | var numInts int | 128 | var numInts int | ||
| 133 | var numFloats int | 129 | var numFloats int | ||
| 134 | var numStack int | 130 | var numStack int | ||
| 135 | - | addStack := func(x uintptr) { | 131 | + | var addStack, addInt, addFloat func(x uintptr) |
| 136 | - | stack[numStack] = x | 132 | + | if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { |
| 137 | - | numStack++ | 133 | + | // Windows arm64 uses the same calling convention as macOS and Linux |
| 138 | - | } | 134 | + | addStack = func(x uintptr) { |
| 139 | - | addInt := func(x uintptr) { | 135 | + | stack[numStack] = x |
| 140 | - | if numInts >= numOfIntegerRegisters() { | 136 | + | numStack++ |
| 141 | - | addStack(x) | 137 | + | } |
| 142 | - | } else { | 138 | + | addInt = func(x uintptr) { |
| 143 | - | sysargs[numInts] = x | 139 | + | if numInts >= numOfIntegerRegisters() { |
| 144 | - | numInts++ | 140 | + | addStack(x) |
| 141 | + | } else { | |||
| 142 | + | sysargs[numInts] = x | |||
| 143 | + | numInts++ | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | addFloat = func(x uintptr) { | |||
| 147 | + | if numFloats < len(floats) { | |||
| 148 | + | floats[numFloats] = x | |||
| 149 | + | numFloats++ | |||
| 150 | + | } else { | |||
| 151 | + | addStack(x) | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | } else { | |||
| 155 | + | // On Windows amd64 the arguments are passed in the numbered registered. | |||
| 156 | + | // So the first int is in the first integer register and the first float | |||
| 157 | + | // is in the second floating register if there is already a first int. | |||
| 158 | + | // This is in contrast to how macOS and Linux pass arguments which | |||
| 159 | + | // tries to use as many registers as possible in the calling convention. | |||
| 160 | + | addStack = func(x uintptr) { | |||
| 161 | + | sysargs[numStack] = x | |||
| 162 | + | numStack++ | |||
| 145 | } | 163 | } | ||
| 164 | + | addInt = addStack | |||
| 165 | + | addFloat = addStack | |||
| 146 | } | 166 | } | ||
| 167 | + | ||||
| 147 | var keepAlive []interface{} | 168 | var keepAlive []interface{} | ||
| 148 | defer func() { | 169 | defer func() { | ||
| 149 | runtime.KeepAlive(keepAlive) | 170 | runtime.KeepAlive(keepAlive) | ||
| @@ -171,33 +192,29 @@ func RegisterFunc(fptr interface{}, cfn uintptr) { | |||||
| 171 | addInt(0) | 192 | addInt(0) | ||
| 172 | } | 193 | } | ||
| 173 | case reflect.Float32: | 194 | case reflect.Float32: | ||
| 174 | - | if numFloats < len(floats) { | 195 | + | addFloat(uintptr(math.Float32bits(float32(v.Float())))) |
| 175 | - | floats[numFloats] = uintptr(math.Float32bits(float32(v.Float()))) | |||
| 176 | - | numFloats++ | |||
| 177 | - | } else { | |||
| 178 | - | addStack(uintptr(math.Float32bits(float32(v.Float())))) | |||
| 179 | - | } | |||
| 180 | case reflect.Float64: | 196 | case reflect.Float64: | ||
| 181 | - | if numFloats < len(floats) { | 197 | + | addFloat(uintptr(math.Float64bits(v.Float()))) |
| 182 | - | floats[numFloats] = uintptr(math.Float64bits(v.Float())) | |||
| 183 | - | numFloats++ | |||
| 184 | - | } else { | |||
| 185 | - | addStack(uintptr(math.Float64bits(v.Float()))) | |||
| 186 | - | } | |||
| 187 | default: | 198 | default: | ||
| 188 | panic("purego: unsupported kind: " + v.Kind().String()) | 199 | panic("purego: unsupported kind: " + v.Kind().String()) | ||
| 189 | } | 200 | } | ||
| 190 | } | 201 | } | ||
| 191 | // TODO: support structs | 202 | // TODO: support structs | ||
| 192 | - | syscall := syscall9Args{ | 203 | + | var r1, r2 uintptr |
| 193 | - | cfn, | 204 | + | if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { |
| 194 | - | sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5], sysargs[6], sysargs[7], sysargs[8], | 205 | + | // Use the normal arm64 calling convention even on Windows |
| 195 | - | floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7], | 206 | + | syscall := syscall9Args{ |
| 196 | - | 0, 0, 0, | 207 | + | cfn, |
| 208 | + | sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5], sysargs[6], sysargs[7], sysargs[8], | |||
| 209 | + | floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7], | |||
| 210 | + | 0, 0, 0, | |||
| 211 | + | } | |||
| 212 | + | runtime_cgocall(syscall9XABI0, unsafe.Pointer(&syscall)) | |||
| 213 | + | r1, r2 = syscall.r1, syscall.r2 | |||
| 214 | + | } else { | |||
| 215 | + | // This is a fallback for amd64, 386, and arm. Note this may not support floats | |||
| 216 | + | r1, r2, _ = syscall_syscall9X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5], sysargs[6], sysargs[7], sysargs[8]) | |||
| 197 | } | 217 | } | ||
| 198 | - | runtime_cgocall(syscall9XABI0, unsafe.Pointer(&syscall)) | |||
| 199 | - | r1, r2 := syscall.r1, syscall.r2 | |||
| 200 | - | ||||
| 201 | if ty.NumOut() == 0 { | 218 | if ty.NumOut() == 0 { | ||
| 202 | return nil | 219 | return nil | ||
| 203 | } | 220 | } | ||
| @@ -240,6 +257,11 @@ func numOfIntegerRegisters() int { | |||||
| 240 | return 8 | 257 | return 8 | ||
| 241 | case "amd64": | 258 | case "amd64": | ||
| 242 | return 6 | 259 | return 6 | ||
| 260 | + | // TODO: figure out why 386 tests are not working | |||
| 261 | + | /*case "386": | |||
| 262 | + | return 0 | |||
| 263 | + | case "arm": | |||
| 264 | + | return 4*/ | |||
| 243 | default: | 265 | default: | ||
| 244 | panic("purego: unknown GOARCH (" + runtime.GOARCH + ")") | 266 | panic("purego: unknown GOARCH (" + runtime.GOARCH + ")") | ||
| 245 | } | 267 | } | ||
A func_test.go +97 -0
| @@ -0,0 +1,97 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2023 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | package purego_test | |
| 5 | + | ||
| 6 | + | import ( | |
| 7 | + | "fmt" | |
| 8 | + | "runtime" | |
| 9 | + | "testing" | |
| 10 | + | "unsafe" | |
| 11 | + | ||
| 12 | + | "github.com/ebitengine/purego" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // This is an internal OS-dependent function for getting the handle to a library | |
| 16 | + | // | |
| 17 | + | //go:linkname openLibrary openLibrary | |
| 18 | + | func openLibrary(name string) (uintptr, error) | |
| 19 | + | ||
| 20 | + | func getSystemLibrary() (string, error) { | |
| 21 | + | switch runtime.GOOS { | |
| 22 | + | case "darwin": | |
| 23 | + | return "/usr/lib/libSystem.B.dylib", nil | |
| 24 | + | case "linux": | |
| 25 | + | return "libc.so.6", nil | |
| 26 | + | case "windows": | |
| 27 | + | return "ucrtbase.dll", nil | |
| 28 | + | default: | |
| 29 | + | return "", fmt.Errorf("GOOS=%s is not supported", runtime.GOOS) | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func TestRegisterFunc(t *testing.T) { | |
| 34 | + | library, err := getSystemLibrary() | |
| 35 | + | if err != nil { | |
| 36 | + | t.Errorf("couldn't get system library: %s", err) | |
| 37 | + | } | |
| 38 | + | libc, err := openLibrary(library) | |
| 39 | + | if err != nil { | |
| 40 | + | t.Errorf("failed to dlopen: %s", err) | |
| 41 | + | } | |
| 42 | + | var puts func(string) | |
| 43 | + | purego.RegisterLibFunc(&puts, libc, "puts") | |
| 44 | + | puts("Calling C from from Go without Cgo!") | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func ExampleNewCallback() { | |
| 48 | + | if runtime.GOOS == "linux" { | |
| 49 | + | // TODO: enable once callbacks are working properly on Linux | |
| 50 | + | fmt.Println("1 2 3 4 5 6 7 8 9\n45") | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | cb := purego.NewCallback(func(a1, a2, a3, a4, a5, a6, a7, a8, a9 int) int { | |
| 55 | + | fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9) | |
| 56 | + | return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 | |
| 57 | + | }) | |
| 58 | + | ||
| 59 | + | var fn func(a1, a2, a3, a4, a5, a6, a7, a8, a9 int) int | |
| 60 | + | purego.RegisterFunc(&fn, cb) | |
| 61 | + | ||
| 62 | + | ret := fn(1, 2, 3, 4, 5, 6, 7, 8, 9) | |
| 63 | + | fmt.Println(ret) | |
| 64 | + | ||
| 65 | + | // Output: 1 2 3 4 5 6 7 8 9 | |
| 66 | + | // 45 | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func Test_qsort(t *testing.T) { | |
| 70 | + | if runtime.GOOS == "linux" { | |
| 71 | + | // TODO: enable once callbacks are working properly on Linux | |
| 72 | + | t.Skip("callbacks are not supported on Linux") | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | library, err := getSystemLibrary() | |
| 76 | + | if err != nil { | |
| 77 | + | t.Errorf("couldn't get system library: %s", err) | |
| 78 | + | } | |
| 79 | + | libc, err := openLibrary(library) | |
| 80 | + | if err != nil { | |
| 81 | + | t.Errorf("failed to dlopen: %s", err) | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | data := []int{88, 56, 100, 2, 25} | |
| 85 | + | sorted := []int{2, 25, 56, 88, 100} | |
| 86 | + | compare := func(a, b *int) int { | |
| 87 | + | return *a - *b | |
| 88 | + | } | |
| 89 | + | var qsort func(data []int, nitms uintptr, size uintptr, compar func(a, b *int) int) | |
| 90 | + | purego.RegisterLibFunc(&qsort, libc, "qsort") | |
| 91 | + | qsort(data, uintptr(len(data)), unsafe.Sizeof(int(0)), compare) | |
| 92 | + | for i := range data { | |
| 93 | + | if data[i] != sorted[i] { | |
| 94 | + | t.Errorf("got %d wanted %d at %d", data[i], sorted[i], i) | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | } | |
| @@ -0,0 +1,97 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2023 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | package purego_test | |||
| 5 | + | ||||
| 6 | + | import ( | |||
| 7 | + | "fmt" | |||
| 8 | + | "runtime" | |||
| 9 | + | "testing" | |||
| 10 | + | "unsafe" | |||
| 11 | + | ||||
| 12 | + | "github.com/ebitengine/purego" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // This is an internal OS-dependent function for getting the handle to a library | |||
| 16 | + | // | |||
| 17 | + | //go:linkname openLibrary openLibrary | |||
| 18 | + | func openLibrary(name string) (uintptr, error) | |||
| 19 | + | ||||
| 20 | + | func getSystemLibrary() (string, error) { | |||
| 21 | + | switch runtime.GOOS { | |||
| 22 | + | case "darwin": | |||
| 23 | + | return "/usr/lib/libSystem.B.dylib", nil | |||
| 24 | + | case "linux": | |||
| 25 | + | return "libc.so.6", nil | |||
| 26 | + | case "windows": | |||
| 27 | + | return "ucrtbase.dll", nil | |||
| 28 | + | default: | |||
| 29 | + | return "", fmt.Errorf("GOOS=%s is not supported", runtime.GOOS) | |||
| 30 | + | } | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func TestRegisterFunc(t *testing.T) { | |||
| 34 | + | library, err := getSystemLibrary() | |||
| 35 | + | if err != nil { | |||
| 36 | + | t.Errorf("couldn't get system library: %s", err) | |||
| 37 | + | } | |||
| 38 | + | libc, err := openLibrary(library) | |||
| 39 | + | if err != nil { | |||
| 40 | + | t.Errorf("failed to dlopen: %s", err) | |||
| 41 | + | } | |||
| 42 | + | var puts func(string) | |||
| 43 | + | purego.RegisterLibFunc(&puts, libc, "puts") | |||
| 44 | + | puts("Calling C from from Go without Cgo!") | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func ExampleNewCallback() { | |||
| 48 | + | if runtime.GOOS == "linux" { | |||
| 49 | + | // TODO: enable once callbacks are working properly on Linux | |||
| 50 | + | fmt.Println("1 2 3 4 5 6 7 8 9\n45") | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | cb := purego.NewCallback(func(a1, a2, a3, a4, a5, a6, a7, a8, a9 int) int { | |||
| 55 | + | fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9) | |||
| 56 | + | return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 | |||
| 57 | + | }) | |||
| 58 | + | ||||
| 59 | + | var fn func(a1, a2, a3, a4, a5, a6, a7, a8, a9 int) int | |||
| 60 | + | purego.RegisterFunc(&fn, cb) | |||
| 61 | + | ||||
| 62 | + | ret := fn(1, 2, 3, 4, 5, 6, 7, 8, 9) | |||
| 63 | + | fmt.Println(ret) | |||
| 64 | + | ||||
| 65 | + | // Output: 1 2 3 4 5 6 7 8 9 | |||
| 66 | + | // 45 | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | func Test_qsort(t *testing.T) { | |||
| 70 | + | if runtime.GOOS == "linux" { | |||
| 71 | + | // TODO: enable once callbacks are working properly on Linux | |||
| 72 | + | t.Skip("callbacks are not supported on Linux") | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | library, err := getSystemLibrary() | |||
| 76 | + | if err != nil { | |||
| 77 | + | t.Errorf("couldn't get system library: %s", err) | |||
| 78 | + | } | |||
| 79 | + | libc, err := openLibrary(library) | |||
| 80 | + | if err != nil { | |||
| 81 | + | t.Errorf("failed to dlopen: %s", err) | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | data := []int{88, 56, 100, 2, 25} | |||
| 85 | + | sorted := []int{2, 25, 56, 88, 100} | |||
| 86 | + | compare := func(a, b *int) int { | |||
| 87 | + | return *a - *b | |||
| 88 | + | } | |||
| 89 | + | var qsort func(data []int, nitms uintptr, size uintptr, compar func(a, b *int) int) | |||
| 90 | + | purego.RegisterLibFunc(&qsort, libc, "qsort") | |||
| 91 | + | qsort(data, uintptr(len(data)), unsafe.Sizeof(int(0)), compare) | |||
| 92 | + | for i := range data { | |||
| 93 | + | if data[i] != sorted[i] { | |||
| 94 | + | t.Errorf("got %d wanted %d at %d", data[i], sorted[i], i) | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | } | |||
M go.mod +2 -0
| @@ -1,3 +1,5 @@ | |||
| 1 | 1 | module github.com/ebitengine/purego | |
| 2 | 2 | ||
| 3 | 3 | go 1.18 | |
| 4 | + | ||
| 5 | + | require golang.org/x/sys v0.7.0 | |
| @@ -1,3 +1,5 @@ | |||||
| 1 | module github.com/ebitengine/purego | 1 | module github.com/ebitengine/purego | ||
| 2 | 2 | ||||
| 3 | go 1.18 | 3 | go 1.18 | ||
| 4 | + | ||||
| 5 | + | require golang.org/x/sys v0.7.0 | |||
A go.sum +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= | |
| 2 | + | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= | |||
| 2 | + | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||
M go_runtime.go +1 -1
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | |
| 3 | 3 | ||
| 4 | - | //go:build darwin || linux | |
| 4 | + | //go:build darwin || linux || windows | |
| 5 | 5 | ||
| 6 | 6 | package purego | |
| 7 | 7 | ||
| @@ -1,7 +1,7 @@ | |||||
| 1 | // SPDX-License-Identifier: Apache-2.0 | 1 | // SPDX-License-Identifier: Apache-2.0 | ||
| 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | ||
| 3 | 3 | ||||
| 4 | - | //go:build darwin || linux | 4 | + | //go:build darwin || linux || windows |
| 5 | 5 | ||||
| 6 | package purego | 6 | package purego | ||
| 7 | 7 | ||||
M sys_arm64.s +1 -56
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | |
| 3 | 3 | ||
| 4 | - | //go:build darwin || (!cgo && linux) | |
| 4 | + | //go:build darwin || (!cgo && linux) || windows | |
| 5 | 5 | ||
| 6 | 6 | #include "textflag.h" | |
| 7 | 7 | #include "go_asm.h" | |
| @@ -62,58 +62,3 @@ TEXT syscall9X(SB), NOSPLIT, $0 | |||
| 62 | 62 | MOVD R0, syscall9Args_r1(R2) // save r1 | |
| 63 | 63 | FMOVD F0, syscall9Args_r2(R2) // save r2 | |
| 64 | 64 | RET | |
| 65 | - | ||
| 66 | - | TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 | |
| 67 | - | NO_LOCAL_POINTERS | |
| 68 | - | ||
| 69 | - | // On entry, the trampoline in zcallback_darwin_arm64.s left | |
| 70 | - | // the callback index in R12 (which is volatile in the C ABI). | |
| 71 | - | ||
| 72 | - | // Save callback register arguments R0-R7 and F0-F7. | |
| 73 | - | // We do this at the top of the frame so they're contiguous with stack arguments. | |
| 74 | - | SUB $(16*8), RSP, R14 | |
| 75 | - | FMOVD F0, (0*8)(R14) | |
| 76 | - | FMOVD F1, (1*8)(R14) | |
| 77 | - | FMOVD F2, (2*8)(R14) | |
| 78 | - | FMOVD F3, (3*8)(R14) | |
| 79 | - | FMOVD F4, (4*8)(R14) | |
| 80 | - | FMOVD F5, (5*8)(R14) | |
| 81 | - | FMOVD F6, (6*8)(R14) | |
| 82 | - | FMOVD F7, (7*8)(R14) | |
| 83 | - | STP (R0, R1), (8*8)(R14) | |
| 84 | - | STP (R2, R3), (10*8)(R14) | |
| 85 | - | STP (R4, R5), (12*8)(R14) | |
| 86 | - | STP (R6, R7), (14*8)(R14) | |
| 87 | - | ||
| 88 | - | // Adjust SP by frame size. | |
| 89 | - | // crosscall2 clobbers FP in the frame record so only save/restore SP. | |
| 90 | - | SUB $(28*8), RSP | |
| 91 | - | MOVD R30, (RSP) | |
| 92 | - | ||
| 93 | - | // Create a struct callbackArgs on our stack. | |
| 94 | - | ADD $(callbackArgs__size + 3*8), RSP, R13 | |
| 95 | - | MOVD R12, callbackArgs_index(R13) // callback index | |
| 96 | - | MOVD R14, R0 | |
| 97 | - | MOVD R0, callbackArgs_args(R13) // address of args vector | |
| 98 | - | MOVD $0, R0 | |
| 99 | - | MOVD R0, callbackArgs_result(R13) // result | |
| 100 | - | ||
| 101 | - | // Move parameters into registers | |
| 102 | - | // Get the ABIInternal function pointer | |
| 103 | - | // without <ABIInternal> by using a closure. | |
| 104 | - | MOVD ·callbackWrap_call(SB), R0 | |
| 105 | - | MOVD (R0), R0 // fn unsafe.Pointer | |
| 106 | - | MOVD R13, R1 // frame (&callbackArgs{...}) | |
| 107 | - | MOVD $0, R3 // ctxt uintptr | |
| 108 | - | ||
| 109 | - | BL crosscall2(SB) | |
| 110 | - | ||
| 111 | - | // Get callback result. | |
| 112 | - | ADD $(callbackArgs__size + 3*8), RSP, R13 | |
| 113 | - | MOVD callbackArgs_result(R13), R0 | |
| 114 | - | ||
| 115 | - | // Restore SP | |
| 116 | - | MOVD (RSP), R30 | |
| 117 | - | ADD $(28*8), RSP | |
| 118 | - | ||
| 119 | - | RET | |
| @@ -1,7 +1,7 @@ | |||||
| 1 | // SPDX-License-Identifier: Apache-2.0 | 1 | // SPDX-License-Identifier: Apache-2.0 | ||
| 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | 2 | // SPDX-FileCopyrightText: 2022 The Ebitengine Authors | ||
| 3 | 3 | ||||
| 4 | - | //go:build darwin || (!cgo && linux) | 4 | + | //go:build darwin || (!cgo && linux) || windows |
| 5 | 5 | ||||
| 6 | #include "textflag.h" | 6 | #include "textflag.h" | ||
| 7 | #include "go_asm.h" | 7 | #include "go_asm.h" | ||
| @@ -62,58 +62,3 @@ TEXT syscall9X(SB), NOSPLIT, $0 | |||||
| 62 | MOVD R0, syscall9Args_r1(R2) // save r1 | 62 | MOVD R0, syscall9Args_r1(R2) // save r1 | ||
| 63 | FMOVD F0, syscall9Args_r2(R2) // save r2 | 63 | FMOVD F0, syscall9Args_r2(R2) // save r2 | ||
| 64 | RET | 64 | RET | ||
| 65 | - | ||||
| 66 | - | TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 | |||
| 67 | - | NO_LOCAL_POINTERS | |||
| 68 | - | ||||
| 69 | - | // On entry, the trampoline in zcallback_darwin_arm64.s left | |||
| 70 | - | // the callback index in R12 (which is volatile in the C ABI). | |||
| 71 | - | ||||
| 72 | - | // Save callback register arguments R0-R7 and F0-F7. | |||
| 73 | - | // We do this at the top of the frame so they're contiguous with stack arguments. | |||
| 74 | - | SUB $(16*8), RSP, R14 | |||
| 75 | - | FMOVD F0, (0*8)(R14) | |||
| 76 | - | FMOVD F1, (1*8)(R14) | |||
| 77 | - | FMOVD F2, (2*8)(R14) | |||
| 78 | - | FMOVD F3, (3*8)(R14) | |||
| 79 | - | FMOVD F4, (4*8)(R14) | |||
| 80 | - | FMOVD F5, (5*8)(R14) | |||
| 81 | - | FMOVD F6, (6*8)(R14) | |||
| 82 | - | FMOVD F7, (7*8)(R14) | |||
| 83 | - | STP (R0, R1), (8*8)(R14) | |||
| 84 | - | STP (R2, R3), (10*8)(R14) | |||
| 85 | - | STP (R4, R5), (12*8)(R14) | |||
| 86 | - | STP (R6, R7), (14*8)(R14) | |||
| 87 | - | ||||
| 88 | - | // Adjust SP by frame size. | |||
| 89 | - | // crosscall2 clobbers FP in the frame record so only save/restore SP. | |||
| 90 | - | SUB $(28*8), RSP | |||
| 91 | - | MOVD R30, (RSP) | |||
| 92 | - | ||||
| 93 | - | // Create a struct callbackArgs on our stack. | |||
| 94 | - | ADD $(callbackArgs__size + 3*8), RSP, R13 | |||
| 95 | - | MOVD R12, callbackArgs_index(R13) // callback index | |||
| 96 | - | MOVD R14, R0 | |||
| 97 | - | MOVD R0, callbackArgs_args(R13) // address of args vector | |||
| 98 | - | MOVD $0, R0 | |||
| 99 | - | MOVD R0, callbackArgs_result(R13) // result | |||
| 100 | - | ||||
| 101 | - | // Move parameters into registers | |||
| 102 | - | // Get the ABIInternal function pointer | |||
| 103 | - | // without <ABIInternal> by using a closure. | |||
| 104 | - | MOVD ·callbackWrap_call(SB), R0 | |||
| 105 | - | MOVD (R0), R0 // fn unsafe.Pointer | |||
| 106 | - | MOVD R13, R1 // frame (&callbackArgs{...}) | |||
| 107 | - | MOVD $0, R3 // ctxt uintptr | |||
| 108 | - | ||||
| 109 | - | BL crosscall2(SB) | |||
| 110 | - | ||||
| 111 | - | // Get callback result. | |||
| 112 | - | ADD $(callbackArgs__size + 3*8), RSP, R13 | |||
| 113 | - | MOVD callbackArgs_result(R13), R0 | |||
| 114 | - | ||||
| 115 | - | // Restore SP | |||
| 116 | - | MOVD (RSP), R30 | |||
| 117 | - | ADD $(28*8), RSP | |||
| 118 | - | ||||
| 119 | - | RET | |||
A sys_unix_arm64.s +64 -0
| @@ -0,0 +1,64 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2023 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | //go:build darwin || (!cgo && linux) | |
| 5 | + | ||
| 6 | + | #include "textflag.h" | |
| 7 | + | #include "go_asm.h" | |
| 8 | + | #include "funcdata.h" | |
| 9 | + | #include "internal/abi/abi_arm64.h" | |
| 10 | + | ||
| 11 | + | TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 | |
| 12 | + | NO_LOCAL_POINTERS | |
| 13 | + | ||
| 14 | + | // On entry, the trampoline in zcallback_darwin_arm64.s left | |
| 15 | + | // the callback index in R12 (which is volatile in the C ABI). | |
| 16 | + | ||
| 17 | + | // Save callback register arguments R0-R7 and F0-F7. | |
| 18 | + | // We do this at the top of the frame so they're contiguous with stack arguments. | |
| 19 | + | SUB $(16*8), RSP, R14 | |
| 20 | + | FMOVD F0, (0*8)(R14) | |
| 21 | + | FMOVD F1, (1*8)(R14) | |
| 22 | + | FMOVD F2, (2*8)(R14) | |
| 23 | + | FMOVD F3, (3*8)(R14) | |
| 24 | + | FMOVD F4, (4*8)(R14) | |
| 25 | + | FMOVD F5, (5*8)(R14) | |
| 26 | + | FMOVD F6, (6*8)(R14) | |
| 27 | + | FMOVD F7, (7*8)(R14) | |
| 28 | + | STP (R0, R1), (8*8)(R14) | |
| 29 | + | STP (R2, R3), (10*8)(R14) | |
| 30 | + | STP (R4, R5), (12*8)(R14) | |
| 31 | + | STP (R6, R7), (14*8)(R14) | |
| 32 | + | ||
| 33 | + | // Adjust SP by frame size. | |
| 34 | + | // crosscall2 clobbers FP in the frame record so only save/restore SP. | |
| 35 | + | SUB $(28*8), RSP | |
| 36 | + | MOVD R30, (RSP) | |
| 37 | + | ||
| 38 | + | // Create a struct callbackArgs on our stack. | |
| 39 | + | ADD $(callbackArgs__size + 3*8), RSP, R13 | |
| 40 | + | MOVD R12, callbackArgs_index(R13) // callback index | |
| 41 | + | MOVD R14, R0 | |
| 42 | + | MOVD R0, callbackArgs_args(R13) // address of args vector | |
| 43 | + | MOVD $0, R0 | |
| 44 | + | MOVD R0, callbackArgs_result(R13) // result | |
| 45 | + | ||
| 46 | + | // Move parameters into registers | |
| 47 | + | // Get the ABIInternal function pointer | |
| 48 | + | // without <ABIInternal> by using a closure. | |
| 49 | + | MOVD ·callbackWrap_call(SB), R0 | |
| 50 | + | MOVD (R0), R0 // fn unsafe.Pointer | |
| 51 | + | MOVD R13, R1 // frame (&callbackArgs{...}) | |
| 52 | + | MOVD $0, R3 // ctxt uintptr | |
| 53 | + | ||
| 54 | + | BL crosscall2(SB) | |
| 55 | + | ||
| 56 | + | // Get callback result. | |
| 57 | + | ADD $(callbackArgs__size + 3*8), RSP, R13 | |
| 58 | + | MOVD callbackArgs_result(R13), R0 | |
| 59 | + | ||
| 60 | + | // Restore SP | |
| 61 | + | MOVD (RSP), R30 | |
| 62 | + | ADD $(28*8), RSP | |
| 63 | + | ||
| 64 | + | RET | |
| @@ -0,0 +1,64 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2023 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | //go:build darwin || (!cgo && linux) | |||
| 5 | + | ||||
| 6 | + | #include "textflag.h" | |||
| 7 | + | #include "go_asm.h" | |||
| 8 | + | #include "funcdata.h" | |||
| 9 | + | #include "internal/abi/abi_arm64.h" | |||
| 10 | + | ||||
| 11 | + | TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 | |||
| 12 | + | NO_LOCAL_POINTERS | |||
| 13 | + | ||||
| 14 | + | // On entry, the trampoline in zcallback_darwin_arm64.s left | |||
| 15 | + | // the callback index in R12 (which is volatile in the C ABI). | |||
| 16 | + | ||||
| 17 | + | // Save callback register arguments R0-R7 and F0-F7. | |||
| 18 | + | // We do this at the top of the frame so they're contiguous with stack arguments. | |||
| 19 | + | SUB $(16*8), RSP, R14 | |||
| 20 | + | FMOVD F0, (0*8)(R14) | |||
| 21 | + | FMOVD F1, (1*8)(R14) | |||
| 22 | + | FMOVD F2, (2*8)(R14) | |||
| 23 | + | FMOVD F3, (3*8)(R14) | |||
| 24 | + | FMOVD F4, (4*8)(R14) | |||
| 25 | + | FMOVD F5, (5*8)(R14) | |||
| 26 | + | FMOVD F6, (6*8)(R14) | |||
| 27 | + | FMOVD F7, (7*8)(R14) | |||
| 28 | + | STP (R0, R1), (8*8)(R14) | |||
| 29 | + | STP (R2, R3), (10*8)(R14) | |||
| 30 | + | STP (R4, R5), (12*8)(R14) | |||
| 31 | + | STP (R6, R7), (14*8)(R14) | |||
| 32 | + | ||||
| 33 | + | // Adjust SP by frame size. | |||
| 34 | + | // crosscall2 clobbers FP in the frame record so only save/restore SP. | |||
| 35 | + | SUB $(28*8), RSP | |||
| 36 | + | MOVD R30, (RSP) | |||
| 37 | + | ||||
| 38 | + | // Create a struct callbackArgs on our stack. | |||
| 39 | + | ADD $(callbackArgs__size + 3*8), RSP, R13 | |||
| 40 | + | MOVD R12, callbackArgs_index(R13) // callback index | |||
| 41 | + | MOVD R14, R0 | |||
| 42 | + | MOVD R0, callbackArgs_args(R13) // address of args vector | |||
| 43 | + | MOVD $0, R0 | |||
| 44 | + | MOVD R0, callbackArgs_result(R13) // result | |||
| 45 | + | ||||
| 46 | + | // Move parameters into registers | |||
| 47 | + | // Get the ABIInternal function pointer | |||
| 48 | + | // without <ABIInternal> by using a closure. | |||
| 49 | + | MOVD ·callbackWrap_call(SB), R0 | |||
| 50 | + | MOVD (R0), R0 // fn unsafe.Pointer | |||
| 51 | + | MOVD R13, R1 // frame (&callbackArgs{...}) | |||
| 52 | + | MOVD $0, R3 // ctxt uintptr | |||
| 53 | + | ||||
| 54 | + | BL crosscall2(SB) | |||
| 55 | + | ||||
| 56 | + | // Get callback result. | |||
| 57 | + | ADD $(callbackArgs__size + 3*8), RSP, R13 | |||
| 58 | + | MOVD callbackArgs_result(R13), R0 | |||
| 59 | + | ||||
| 60 | + | // Restore SP | |||
| 61 | + | MOVD (RSP), R30 | |||
| 62 | + | ADD $(28*8), RSP | |||
| 63 | + | ||||
| 64 | + | RET | |||
M syscall_windows.go +21 -0
| @@ -5,8 +5,19 @@ package purego | |||
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | 7 | "syscall" | |
| 8 | + | _ "unsafe" // only for go:linkname | |
| 9 | + | ||
| 10 | + | "golang.org/x/sys/windows" | |
| 8 | 11 | ) | |
| 9 | 12 | ||
| 13 | + | var syscall9XABI0 uintptr | |
| 14 | + | ||
| 15 | + | type syscall9Args struct { | |
| 16 | + | fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr | |
| 17 | + | f1, f2, f3, f4, f5, f6, f7, f8 uintptr | |
| 18 | + | r1, r2, err uintptr | |
| 19 | + | } | |
| 20 | + | ||
| 10 | 21 | func syscall_syscall9X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, err uintptr) { | |
| 11 | 22 | r1, r2, errno := syscall.Syscall9(fn, 9, a1, a2, a3, a4, a5, a6, a7, a8, a9) | |
| 12 | 23 | return r1, r2, uintptr(errno) | |
| @@ -22,3 +33,13 @@ func syscall_syscall9X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, | |||
| 22 | 33 | func NewCallback(fn interface{}) uintptr { | |
| 23 | 34 | return syscall.NewCallback(fn) | |
| 24 | 35 | } | |
| 36 | + | ||
| 37 | + | //go:linkname openLibrary openLibrary | |
| 38 | + | func openLibrary(name string) (uintptr, error) { | |
| 39 | + | handle, err := windows.LoadLibrary(name) | |
| 40 | + | return uintptr(handle), err | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func loadSymbol(handle uintptr, name string) (uintptr, error) { | |
| 44 | + | return windows.GetProcAddress(windows.Handle(handle), name) | |
| 45 | + | } | |
| @@ -5,8 +5,19 @@ package purego | |||||
| 5 | 5 | ||||
| 6 | import ( | 6 | import ( | ||
| 7 | "syscall" | 7 | "syscall" | ||
| 8 | + | _ "unsafe" // only for go:linkname | |||
| 9 | + | ||||
| 10 | + | "golang.org/x/sys/windows" | |||
| 8 | ) | 11 | ) | ||
| 9 | 12 | ||||
| 13 | + | var syscall9XABI0 uintptr | |||
| 14 | + | ||||
| 15 | + | type syscall9Args struct { | |||
| 16 | + | fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr | |||
| 17 | + | f1, f2, f3, f4, f5, f6, f7, f8 uintptr | |||
| 18 | + | r1, r2, err uintptr | |||
| 19 | + | } | |||
| 20 | + | ||||
| 10 | func syscall_syscall9X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, err uintptr) { | 21 | func syscall_syscall9X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, err uintptr) { | ||
| 11 | r1, r2, errno := syscall.Syscall9(fn, 9, a1, a2, a3, a4, a5, a6, a7, a8, a9) | 22 | r1, r2, errno := syscall.Syscall9(fn, 9, a1, a2, a3, a4, a5, a6, a7, a8, a9) | ||
| 12 | return r1, r2, uintptr(errno) | 23 | return r1, r2, uintptr(errno) | ||
| @@ -22,3 +33,13 @@ func syscall_syscall9X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2, | |||||
| 22 | func NewCallback(fn interface{}) uintptr { | 33 | func NewCallback(fn interface{}) uintptr { | ||
| 23 | return syscall.NewCallback(fn) | 34 | return syscall.NewCallback(fn) | ||
| 24 | } | 35 | } | ||
| 36 | + | ||||
| 37 | + | //go:linkname openLibrary openLibrary | |||
| 38 | + | func openLibrary(name string) (uintptr, error) { | |||
| 39 | + | handle, err := windows.LoadLibrary(name) | |||
| 40 | + | return uintptr(handle), err | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func loadSymbol(handle uintptr, name string) (uintptr, error) { | |||
| 44 | + | return windows.GetProcAddress(windows.Handle(handle), name) | |||
| 45 | + | } | |||