1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3
4package purego_test
5
6import (
7 "bytes"
8 "errors"
9 "fmt"
10 "os/exec"
11 "path/filepath"
12 "runtime"
13 "strings"
14 "testing"
15 "unsafe"
16
17 "github.com/bnema/purego"
18 "github.com/bnema/purego/internal/load"
19)
20
21func getSystemLibrary() (string, error) {
22 switch runtime.GOOS {
23 case "darwin":
24 return "/usr/lib/libSystem.B.dylib", nil
25 case "freebsd":
26 return "libc.so.7", nil
27 case "linux":
28 return "libc.so.6", nil
29 case "netbsd":
30 return "libc.so", nil
31 case "windows":
32 return "ucrtbase.dll", nil
33 default:
34 return "", fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)
35 }
36}
37
38func TestRegisterFunc(t *testing.T) {
39 library, err := getSystemLibrary()
40 if err != nil {
41 t.Fatalf("couldn't get system library: %s", err)
42 }
43 libc, err := load.OpenLibrary(library)
44 if err != nil {
45 t.Fatalf("failed to dlopen: %s", err)
46 }
47 var puts func(string)
48 purego.RegisterLibFunc(&puts, libc, "puts")
49 puts("Calling C from from Go without Cgo!")
50}
51
52func Test_qsort(t *testing.T) {
53 if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
54 t.Skip("Platform doesn't support Floats")
55 return
56 }
57 library, err := getSystemLibrary()
58 if err != nil {
59 t.Fatalf("couldn't get system library: %s", err)
60 }
61 libc, err := load.OpenLibrary(library)
62 if err != nil {
63 t.Fatalf("failed to dlopen: %s", err)
64 }
65
66 data := []int{88, 56, 100, 2, 25}
67 sorted := []int{2, 25, 56, 88, 100}
68 compare := func(_ purego.CDecl, a, b *int) int {
69 return *a - *b
70 }
71 var qsort func(data []int, nitms uintptr, size uintptr, compar func(_ purego.CDecl, a, b *int) int)
72 purego.RegisterLibFunc(&qsort, libc, "qsort")
73 qsort(data, uintptr(len(data)), unsafe.Sizeof(int(0)), compare)
74 for i := range data {
75 if data[i] != sorted[i] {
76 t.Errorf("got %d wanted %d at %d", data[i], sorted[i], i)
77 }
78 }
79}
80
81func TestRegisterFunc_Floats(t *testing.T) {
82 if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
83 t.Skip("Platform doesn't support Floats")
84 return
85 }
86 if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
87 t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
88 }
89 library, err := getSystemLibrary()
90 if err != nil {
91 t.Fatalf("couldn't get system library: %s", err)
92 }
93 libc, err := load.OpenLibrary(library)
94 if err != nil {
95 t.Fatalf("failed to dlopen: %s", err)
96 }
97 {
98 var strtof func(arg string) float32
99 purego.RegisterLibFunc(&strtof, libc, "strtof")
100 const (
101 arg = "2"
102 )
103 got := strtof(arg)
104 expected := float32(2)
105 if got != expected {
106 t.Errorf("strtof failed. got %f but wanted %f", got, expected)
107 }
108 }
109 {
110 var strtod func(arg string, ptr **byte) float64
111 purego.RegisterLibFunc(&strtod, libc, "strtod")
112 const (
113 arg = "1"
114 )
115 got := strtod(arg, nil)
116 expected := float64(1)
117 if got != expected {
118 t.Errorf("strtod failed. got %f but wanted %f", got, expected)
119 }
120 }
121}
122
123func TestRegisterLibFunc_Bool(t *testing.T) {
124 if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
125 t.Skip("Platform doesn't support callbacks")
126 return
127 }
128 // this callback recreates the state where the return register
129 // contains other information but the least significant byte is false
130 cbFalse := purego.NewCallback(func() uintptr {
131 x := uint64(0x7F5948AE9A00)
132 return uintptr(x)
133 })
134 var runFalse func() bool
135 purego.RegisterFunc(&runFalse, cbFalse)
136 expected := false
137 if got := runFalse(); got != expected {
138 t.Errorf("runFalse failed. got %t but wanted %t", got, expected)
139 }
140}
141
142func TestABI(t *testing.T) {
143 if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
144 t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
145 }
146 libFileName := filepath.Join(t.TempDir(), "abitest.so")
147 t.Logf("Build %v", libFileName)
148
149 if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
150 t.Fatal(err)
151 }
152
153 lib, err := load.OpenLibrary(libFileName)
154 if err != nil {
155 t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
156 }
157 defer func() {
158 if err := load.CloseLibrary(lib); err != nil {
159 t.Fatalf("failed to close library: %s", err)
160 }
161 }()
162 {
163 const cName = "stack_uint8_t"
164 const expect = 2047
165 var fn func(a, b, c, d, e, f, g, h uint32, i, j uint8, k uint32) uint32
166 purego.RegisterLibFunc(&fn, lib, cName)
167 res := fn(256, 512, 4, 8, 16, 32, 64, 128, 1, 2, 1024)
168 if res != expect {
169 t.Fatalf("%s: got %d, want %d", cName, res, expect)
170 }
171 }
172 {
173 const cName = "reg_uint8_t"
174 const expect = 1027
175 var fn func(a, b uint8, c uint32) uint32
176 purego.RegisterLibFunc(&fn, lib, cName)
177 res := fn(1, 2, 1024)
178 if res != expect {
179 t.Fatalf("%s: got %d, want %d", cName, res, expect)
180 }
181 }
182 {
183 const cName = "stack_string"
184 const expect = 255
185 var fn func(a, b, c, d, e, f, g, h uint32, i string) uint32
186 purego.RegisterLibFunc(&fn, lib, cName)
187 res := fn(1, 2, 4, 8, 16, 32, 64, 128, "test")
188 if res != expect {
189 t.Fatalf("%s: got %d, want %d", cName, res, expect)
190 }
191 }
192 {
193 const cName = "stack_8i32_3strings"
194 var fn func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, string, string)
195 purego.RegisterLibFunc(&fn, lib, cName)
196 buf := make([]byte, 256)
197 fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz")
198 res := string(buf[:bytes.IndexByte(buf, 0)])
199 const want = "1:2:3:4:5:6:7:8:foo:bar:baz"
200 if res != want {
201 t.Fatalf("%s: got %q, want %q", cName, res, want)
202 }
203 }
204}
205
206func TestABI_ArgumentPassing(t *testing.T) {
207 if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
208 t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
209 }
210 libFileName := filepath.Join(t.TempDir(), "abitest.so")
211 if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
212 t.Fatal(err)
213 }
214 lib, err := load.OpenLibrary(libFileName)
215 if err != nil {
216 t.Fatalf("Failed to open library %q: %v", libFileName, err)
217 }
218 t.Cleanup(func() {
219 if err := load.CloseLibrary(lib); err != nil {
220 t.Errorf("Failed to close library: %v", err)
221 }
222 })
223
224 tests := []struct {
225 name string
226 fn any
227 cFn string
228 call func(any) string
229 want string
230 }{
231 {
232 name: "10_int32_baseline",
233 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
234 cFn: "stack_10_int32",
235 call: func(f any) string {
236 buf := make([]byte, 256)
237 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
238 return string(buf[:bytes.IndexByte(buf, 0)])
239 },
240 want: "1:2:3:4:5:6:7:8:9:10",
241 },
242 {
243 name: "11_int32",
244 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
245 cFn: "stack_11_int32",
246 call: func(f any) string {
247 buf := make([]byte, 256)
248 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
249 return string(buf[:bytes.IndexByte(buf, 0)])
250 },
251 want: "1:2:3:4:5:6:7:8:9:10:11",
252 },
253 {
254 name: "10_float32",
255 fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)),
256 cFn: "stack_10_float32",
257 call: func(f any) string {
258 buf := make([]byte, 256)
259 (*f.(*func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)))(&buf[0], 256, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0)
260 return string(buf[:bytes.IndexByte(buf, 0)])
261 },
262 want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
263 },
264 {
265 name: "mixed_stack_strings",
266 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)),
267 cFn: "stack_mixed_stack_4args",
268 call: func(f any) string {
269 buf := make([]byte, 256)
270 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, "foo", false, 99, "bar")
271 return string(buf[:bytes.IndexByte(buf, 0)])
272 },
273 want: "1:2:3:4:5:6:7:8:foo:0:99:bar",
274 },
275 {
276 name: "20_int32",
277 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
278 cFn: "stack_20_int32",
279 call: func(f any) string {
280 buf := make([]byte, 512)
281 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 512, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
282 return string(buf[:bytes.IndexByte(buf, 0)])
283 },
284 want: "1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20",
285 },
286 {
287 name: "8int_hfa2_stack",
288 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
289 cFn: "stack_8int_hfa2_stack",
290 call: func(f any) string {
291 buf := make([]byte, 256)
292 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y float32 }{10.0, 20.0})
293 return string(buf[:bytes.IndexByte(buf, 0)])
294 },
295 want: "1:2:3:4:5:6:7:8:10.0:20.0",
296 },
297 {
298 name: "8int_2structs_stack",
299 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })),
300 cFn: "stack_8int_2structs_stack",
301 call: func(f any) string {
302 buf := make([]byte, 256)
303 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y int32 }{9, 10}, struct{ x, y int32 }{11, 12})
304 return string(buf[:bytes.IndexByte(buf, 0)])
305 },
306 want: "1:2:3:4:5:6:7:8:9:10:11:12",
307 },
308 {
309 name: "8float_hfa2_stack",
310 fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })),
311 cFn: "stack_8float_hfa2_stack",
312 call: func(f any) string {
313 buf := make([]byte, 256)
314 (*f.(*func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })))(&buf[0], 256, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, struct{ x, y float32 }{9.0, 10.0})
315 return string(buf[:bytes.IndexByte(buf, 0)])
316 },
317 want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
318 },
319 {
320 name: "8int_hfa2_floatregs",
321 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
322 cFn: "stack_8int_hfa2_floatregs",
323 call: func(f any) string {
324 buf := make([]byte, 256)
325 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y float32 }{10.0, 20.0})
326 return string(buf[:bytes.IndexByte(buf, 0)])
327 },
328 want: "1:2:3:4:5:6:7:8:10.0:20.0",
329 },
330 {
331 name: "8int_int_struct_int",
332 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)),
333 cFn: "stack_8int_int_struct_int",
334 call: func(f any) string {
335 buf := make([]byte, 256)
336 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, struct{ x, y int32 }{10, 11}, 12)
337 return string(buf[:bytes.IndexByte(buf, 0)])
338 },
339 want: "1:2:3:4:5:6:7:8:9:10:11:12",
340 },
341 {
342 name: "8int_hfa4_stack",
343 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })),
344 cFn: "stack_8int_hfa4_stack",
345 call: func(f any) string {
346 buf := make([]byte, 256)
347 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y, z, w float32 }{10.0, 20.0, 30.0, 40.0})
348 return string(buf[:bytes.IndexByte(buf, 0)])
349 },
350 want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0",
351 },
352 {
353 name: "8int_mixed_struct",
354 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
355 a int32
356 b float32
357 })),
358 cFn: "stack_8int_mixed_struct",
359 call: func(f any) string {
360 buf := make([]byte, 256)
361 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
362 a int32
363 b float32
364 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
365 a int32
366 b float32
367 }{9, 10.0})
368 return string(buf[:bytes.IndexByte(buf, 0)])
369 },
370 want: "1:2:3:4:5:6:7:8:9:10.0",
371 },
372 }
373
374 for _, tt := range tests {
375 t.Run(tt.name, func(t *testing.T) {
376 if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
377 t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
378 }
379 if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
380 t.Skip("float32 stack arguments not yet supported on this platform")
381 }
382 // Struct tests require Darwin ARM64 or AMD64
383 if strings.HasPrefix(tt.name, "8int_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
384 t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
385 }
386 if strings.HasPrefix(tt.name, "8float_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
387 t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
388 }
389
390 purego.RegisterLibFunc(tt.fn, lib, tt.cFn)
391 got := tt.call(tt.fn)
392 if got != tt.want {
393 t.Errorf("%s\n got: %q\n want: %q", tt.cFn, got, tt.want)
394 }
395 })
396 }
397}
398
399func TestABI_TooManyArguments(t *testing.T) {
400 if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
401 t.Skip("This test is specific to Darwin ARM64")
402 }
403
404 libFileName := filepath.Join(t.TempDir(), "abitest.so")
405 if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
406 t.Fatal(err)
407 }
408 lib, err := load.OpenLibrary(libFileName)
409 if err != nil {
410 t.Fatalf("Failed to open library %q: %v", libFileName, err)
411 }
412 t.Cleanup(func() {
413 if err := load.CloseLibrary(lib); err != nil {
414 t.Errorf("Failed to close library: %v", err)
415 }
416 })
417
418 // Test that 35 int64 arguments (27 slots needed) exceeds the limit
419 t.Run("35_int64_exceeds_limit", func(t *testing.T) {
420 defer func() {
421 if r := recover(); r != nil {
422 t.Logf("Got expected panic: %v", r)
423 } else {
424 t.Errorf("Expected panic but didn't get one")
425 }
426 }()
427
428 var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64)
429 purego.RegisterLibFunc(&fn, lib, "stack_35_int64_exceeds")
430 })
431}
432
433func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
434 out, err := exec.Command("go", "env", compilerEnv).Output()
435 if err != nil {
436 return fmt.Errorf("go env %s error: %w", compilerEnv, err)
437 }
438
439 compiler := strings.TrimSpace(string(out))
440 if compiler == "" {
441 return errors.New("compiler not found")
442 }
443
444 args := []string{"-shared", "-Wall", "-Werror", "-fPIC", "-o", libFile}
445 if runtime.GOARCH == "386" {
446 args = append(args, "-m32")
447 }
448 // macOS arm64 can run amd64 tests through Rossetta.
449 // Build the shared library based on the GOARCH and not
450 // the default behavior of the compiler.
451 if runtime.GOOS == "darwin" {
452 var arch string
453 switch runtime.GOARCH {
454 case "arm64":
455 arch = "arm64"
456 case "amd64":
457 arch = "x86_64"
458 default:
459 return fmt.Errorf("unknown macOS architecture %s", runtime.GOARCH)
460 }
461 args = append(args, "-arch", arch)
462 }
463 cmd := exec.Command(compiler, append(args, sources...)...)
464 if out, err := cmd.CombinedOutput(); err != nil {
465 return fmt.Errorf("compile lib: %w\n%q\n%s", err, cmd, string(out))
466 }
467
468 return nil
469}