v0.10.2 7f6f138

func_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close