v0.10.0-alpha.5 585a4c5

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	"testing"
15	"unsafe"
16
17	"github.com/ebitengine/purego"
18	"github.com/ebitengine/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}

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