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