main 3a10209

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	"math"
11	"os"
12	"os/exec"
13	"path/filepath"
14	"reflect"
15	"runtime"
16	"strconv"
17	"strings"
18	"structs"
19	"sync"
20	"testing"
21	"unsafe"
22
23	"github.com/bnema/purego"
24	"github.com/bnema/purego/internal/load"
25)
26
27func getSystemLibrary() (string, error) {
28	switch runtime.GOOS {
29	case "android":
30		return "libc.so", nil
31	case "darwin", "ios":
32		return "/usr/lib/libSystem.B.dylib", nil
33	case "freebsd":
34		return "libc.so.7", nil
35	case "linux":
36		return "libc.so.6", nil
37	case "netbsd":
38		return "libc.so", nil
39	case "windows":
40		return "ucrtbase.dll", nil
41	default:
42		return "", fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)
43	}
44}
45
46func TestRegisterFunc_ConcurrentPointerReturn(t *testing.T) {
47	library, err := getSystemLibrary()
48	if err != nil {
49		t.Fatalf("couldn't get system library: %s", err)
50	}
51	libc, err := load.OpenLibrary(library)
52	if err != nil {
53		t.Fatalf("failed to dlopen: %s", err)
54	}
55
56	var alloc func(uint64) *byte
57	var free func(*byte)
58	purego.RegisterLibFunc(&alloc, libc, "malloc")
59	purego.RegisterLibFunc(&free, libc, "free")
60
61	var wg sync.WaitGroup
62
63	for i := range runtime.NumCPU() {
64		wg.Add(1)
65		go func(id int) {
66			defer wg.Done()
67			for range 400_000 {
68				ptr := alloc(5)
69				if ptr == nil {
70					continue
71				}
72				free(ptr)
73			}
74		}(i)
75	}
76
77	wg.Wait()
78}
79
80func TestRegisterFunc(t *testing.T) {
81	library, err := getSystemLibrary()
82	if err != nil {
83		t.Fatalf("couldn't get system library: %s", err)
84	}
85	libc, err := load.OpenLibrary(library)
86	if err != nil {
87		t.Fatalf("failed to dlopen: %s", err)
88	}
89	var puts func(string)
90	purego.RegisterLibFunc(&puts, libc, "puts")
91	puts("Calling C from from Go without Cgo!")
92}
93
94func Test_qsort(t *testing.T) {
95	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" {
96		t.Skip("Platform doesn't support Floats")
97		return
98	}
99	library, err := getSystemLibrary()
100	if err != nil {
101		t.Fatalf("couldn't get system library: %s", err)
102	}
103	libc, err := load.OpenLibrary(library)
104	if err != nil {
105		t.Fatalf("failed to dlopen: %s", err)
106	}
107
108	data := []int{88, 56, 100, 2, 25}
109	sorted := []int{2, 25, 56, 88, 100}
110	compare := func(_ purego.CDecl, a, b *int) int {
111		return *a - *b
112	}
113	var qsort func(data []int, nitms uintptr, size uintptr, compar func(_ purego.CDecl, a, b *int) int)
114	purego.RegisterLibFunc(&qsort, libc, "qsort")
115	qsort(data, uintptr(len(data)), unsafe.Sizeof(int(0)), compare)
116	for i := range data {
117		if data[i] != sorted[i] {
118			t.Errorf("got %d wanted %d at %d", data[i], sorted[i], i)
119		}
120	}
121}
122
123func TestRegisterFunc_Floats(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 Floats")
126		return
127	}
128	if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
129		t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
130	}
131	library, err := getSystemLibrary()
132	if err != nil {
133		t.Fatalf("couldn't get system library: %s", err)
134	}
135	libc, err := load.OpenLibrary(library)
136	if err != nil {
137		t.Fatalf("failed to dlopen: %s", err)
138	}
139	{
140		var strtof func(arg string) float32
141		purego.RegisterLibFunc(&strtof, libc, "strtof")
142		const (
143			arg = "2"
144		)
145		got := strtof(arg)
146		expected := float32(2)
147		if got != expected {
148			t.Errorf("strtof failed. got %f but wanted %f", got, expected)
149		}
150	}
151	{
152		var strtod func(arg string, ptr **byte) float64
153		purego.RegisterLibFunc(&strtod, libc, "strtod")
154		const (
155			arg = "1"
156		)
157		got := strtod(arg, nil)
158		expected := float64(1)
159		if got != expected {
160			t.Errorf("strtod failed. got %f but wanted %f", got, expected)
161		}
162	}
163}
164
165func TestRegisterLibFunc_Bool(t *testing.T) {
166	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" {
167		t.Skip("Platform doesn't support callbacks")
168		return
169	}
170	// this callback recreates the state where the return register
171	// contains other information but the least significant byte is false
172	cbFalse := purego.NewCallback(func() uintptr {
173		x := uint64(0x7F5948AE9A00)
174		return uintptr(x)
175	})
176	var runFalse func() bool
177	purego.RegisterFunc(&runFalse, cbFalse)
178	expected := false
179	if got := runFalse(); got != expected {
180		t.Errorf("runFalse failed. got %t but wanted %t", got, expected)
181	}
182}
183
184func TestNewCallback_NotAFunction(t *testing.T) {
185	for _, tc := range []struct {
186		name string
187		fn   any
188	}{
189		{"nil", nil},
190		{"int", 42},
191		{"string", "not a function"},
192		{"pointer", new(int)},
193	} {
194		t.Run(tc.name, func(t *testing.T) {
195			defer func() {
196				r := recover()
197				if r == nil {
198					t.Fatal("NewCallback did not panic")
199				}
200				const want = "purego: the type must be a function but was not"
201				if got := fmt.Sprint(r); got != want {
202					t.Fatalf("panic mismatch:\n  got:  %q\n  want: %q", got, want)
203				}
204			}()
205			purego.NewCallback(tc.fn)
206		})
207	}
208}
209
210func TestRegisterFunc_InvalidFunctionPointer(t *testing.T) {
211	for _, tc := range []struct {
212		name string
213		fptr any
214		want string
215	}{
216		{"nil", nil, "purego: fptr must be a non-nil function pointer"},
217		{"non_pointer", 42, "purego: fptr must be a non-nil function pointer"},
218		{"function_value", func() {}, "purego: fptr must be a non-nil function pointer"},
219		{"nil_function_pointer", (*func())(nil), "purego: fptr must be a non-nil function pointer"},
220		{"pointer_to_non_function", new(int), "purego: fptr must be a function pointer"},
221		{"pointer_to_function_pointer", new(*func()), "purego: fptr must be a function pointer"},
222	} {
223		t.Run(tc.name, func(t *testing.T) {
224			defer func() {
225				r := recover()
226				if r == nil {
227					t.Fatal("RegisterFunc did not panic")
228				}
229				if got := fmt.Sprint(r); got != tc.want {
230					t.Fatalf("panic mismatch:\n  got:  %q\n  want: %q", got, tc.want)
231				}
232			}()
233			purego.RegisterFunc(tc.fptr, 1)
234		})
235	}
236}
237
238func TestABI(t *testing.T) {
239	libFileName := filepath.Join(t.TempDir(), "abitest.so")
240	t.Logf("Build %v", libFileName)
241
242	if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
243		t.Fatal(err)
244	}
245
246	lib, err := load.OpenLibrary(libFileName)
247	if err != nil {
248		t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
249	}
250	defer func() {
251		if err := load.CloseLibrary(lib); err != nil {
252			t.Fatalf("failed to close library: %s", err)
253		}
254	}()
255	{
256		const cName = "stack_uint8_t"
257		const expect = 2047
258		var fn func(a, b, c, d, e, f, g, h uint32, i, j uint8, k uint32) uint32
259		purego.RegisterLibFunc(&fn, lib, cName)
260		res := fn(256, 512, 4, 8, 16, 32, 64, 128, 1, 2, 1024)
261		if res != expect {
262			t.Fatalf("%s: got %d, want %d", cName, res, expect)
263		}
264	}
265	{
266		const cName = "reg_uint8_t"
267		const expect = 1027
268		var fn func(a, b uint8, c uint32) uint32
269		purego.RegisterLibFunc(&fn, lib, cName)
270		res := fn(1, 2, 1024)
271		if res != expect {
272			t.Fatalf("%s: got %d, want %d", cName, res, expect)
273		}
274	}
275	{
276		const cName = "stack_string"
277		const expect = 255
278		var fn func(a, b, c, d, e, f, g, h uint32, i string) uint32
279		purego.RegisterLibFunc(&fn, lib, cName)
280		res := fn(1, 2, 4, 8, 16, 32, 64, 128, "test")
281		if res != expect {
282			t.Fatalf("%s: got %d, want %d", cName, res, expect)
283		}
284	}
285	{
286		const cName = "stack_8i32_3strings"
287		var fn func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, string, string)
288		purego.RegisterLibFunc(&fn, lib, cName)
289		buf := make([]byte, 256)
290		fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz")
291		res := string(buf[:bytes.IndexByte(buf, 0)])
292		const want = "1:2:3:4:5:6:7:8:foo:bar:baz"
293		if res != want {
294			t.Fatalf("%s: got %q, want %q", cName, res, want)
295		}
296	}
297	{
298		const cName = "return_func_ptr"
299		var fn func() func(a, b int32) int32
300		purego.RegisterLibFunc(&fn, lib, cName)
301		add := fn()
302		const expect = 5
303		if res := add(2, 3); res != expect {
304			t.Fatalf("%s: got %d, want %d", cName, res, expect)
305		}
306	}
307	{
308		const cName = "return_null_func_ptr"
309		var fn func() func(a, b int32) int32
310		purego.RegisterLibFunc(&fn, lib, cName)
311		if fn() != nil {
312			t.Fatalf("%s: got a non-nil func, want nil", cName)
313		}
314	}
315}
316
317func TestABI_ArgumentPassing(t *testing.T) {
318	libFileName := filepath.Join(t.TempDir(), "abitest.so")
319	if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
320		t.Fatal(err)
321	}
322	lib, err := load.OpenLibrary(libFileName)
323	if err != nil {
324		t.Fatalf("Failed to open library %q: %v", libFileName, err)
325	}
326	t.Cleanup(func() {
327		if err := load.CloseLibrary(lib); err != nil {
328			t.Errorf("Failed to close library: %v", err)
329		}
330	})
331
332	tests := []struct {
333		name string
334		fn   any
335		cFn  string
336		call func(any) string
337		want string
338	}{
339		{
340			name: "10_int32_baseline",
341			fn:   new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
342			cFn:  "stack_10_int32",
343			call: func(f any) string {
344				buf := make([]byte, 256)
345				(*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)
346				return string(buf[:bytes.IndexByte(buf, 0)])
347			},
348			want: "1:2:3:4:5:6:7:8:9:10",
349		},
350		{
351			name: "11_int32",
352			fn:   new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
353			cFn:  "stack_11_int32",
354			call: func(f any) string {
355				buf := make([]byte, 256)
356				(*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)
357				return string(buf[:bytes.IndexByte(buf, 0)])
358			},
359			want: "1:2:3:4:5:6:7:8:9:10:11",
360		},
361		{
362			name: "10_float32",
363			fn:   new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)),
364			cFn:  "stack_10_float32",
365			call: func(f any) string {
366				buf := make([]byte, 256)
367				(*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)
368				return string(buf[:bytes.IndexByte(buf, 0)])
369			},
370			want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
371		},
372		{
373			name: "mixed_stack_strings",
374			fn:   new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)),
375			cFn:  "stack_mixed_stack_4args",
376			call: func(f any) string {
377				buf := make([]byte, 256)
378				(*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")
379				return string(buf[:bytes.IndexByte(buf, 0)])
380			},
381			want: "1:2:3:4:5:6:7:8:foo:0:99:bar",
382		},
383		{
384			name: "20_int32",
385			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)),
386			cFn:  "stack_20_int32",
387			call: func(f any) string {
388				buf := make([]byte, 512)
389				(*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)
390				return string(buf[:bytes.IndexByte(buf, 0)])
391			},
392			want: "1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20",
393		},
394		{
395			name: "8int_hfa2_stack",
396			fn:   new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
397			cFn:  "stack_8int_hfa2_stack",
398			call: func(f any) string {
399				buf := make([]byte, 256)
400				(*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})
401				return string(buf[:bytes.IndexByte(buf, 0)])
402			},
403			want: "1:2:3:4:5:6:7:8:10.0:20.0",
404		},
405		{
406			name: "8int_2structs_stack",
407			fn:   new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })),
408			cFn:  "stack_8int_2structs_stack",
409			call: func(f any) string {
410				buf := make([]byte, 256)
411				(*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})
412				return string(buf[:bytes.IndexByte(buf, 0)])
413			},
414			want: "1:2:3:4:5:6:7:8:9:10:11:12",
415		},
416		{
417			name: "8float_hfa2_stack",
418			fn:   new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })),
419			cFn:  "stack_8float_hfa2_stack",
420			call: func(f any) string {
421				buf := make([]byte, 256)
422				(*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})
423				return string(buf[:bytes.IndexByte(buf, 0)])
424			},
425			want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
426		},
427		{
428			name: "8int_hfa2_floatregs",
429			fn:   new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
430			cFn:  "stack_8int_hfa2_floatregs",
431			call: func(f any) string {
432				buf := make([]byte, 256)
433				(*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})
434				return string(buf[:bytes.IndexByte(buf, 0)])
435			},
436			want: "1:2:3:4:5:6:7:8:10.0:20.0",
437		},
438		{
439			name: "8int_int_struct_int",
440			fn:   new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)),
441			cFn:  "stack_8int_int_struct_int",
442			call: func(f any) string {
443				buf := make([]byte, 256)
444				(*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)
445				return string(buf[:bytes.IndexByte(buf, 0)])
446			},
447			want: "1:2:3:4:5:6:7:8:9:10:11:12",
448		},
449		{
450			name: "8int_hfa4_stack",
451			fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
452				_          structs.HostLayout
453				x, y, z, w float32
454			})),
455			cFn: "stack_8int_hfa4_stack",
456			call: func(f any) string {
457				buf := make([]byte, 256)
458				(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
459					_          structs.HostLayout
460					x, y, z, w float32
461				})))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
462					_          structs.HostLayout
463					x, y, z, w float32
464				}{x: 10.0, y: 20.0, z: 30.0, w: 40.0})
465				return string(buf[:bytes.IndexByte(buf, 0)])
466			},
467			want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0",
468		},
469		{
470			name: "8int_mixed_struct",
471			fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
472				_ structs.HostLayout
473				a int32
474				b float32
475			})),
476			cFn: "stack_8int_mixed_struct",
477			call: func(f any) string {
478				buf := make([]byte, 256)
479				(*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
480					_ structs.HostLayout
481					a int32
482					b float32
483				})))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
484					_ structs.HostLayout
485					a int32
486					b float32
487				}{a: 9, b: 10.0})
488				return string(buf[:bytes.IndexByte(buf, 0)])
489			},
490			want: "1:2:3:4:5:6:7:8:9:10.0",
491		},
492		{
493			// check if unaligned 64bit argument and 64bit returned value is properly passed via registers
494			// arm-specific but must work everywhere
495			name: "arm_int64_unaligned_in_registers",
496			fn:   new(func(uintptr, int64) int64),
497			cFn:  "arm_int64_unaligned_in_registers",
498			call: func(f any) string {
499				fn := *(f).(*func(x uintptr, y int64) int64)
500				return strconv.FormatInt(fn(456, math.MaxInt32+1500), 10)
501			},
502			want: strconv.FormatInt(456*123+math.MaxInt32+1500, 10),
503		},
504		{
505			// check if unaligned 64bit argument and 64bit returned value is properly passed via stack
506			// arm-specific but must work everywhere
507			name: "arm_int64_unaligned_on_stack",
508			fn:   new(func(uintptr, uintptr, uintptr, uintptr, uintptr, int64) int64),
509			cFn:  "arm_int64_unaligned_on_stack",
510			call: func(f any) string {
511				fn := *(f).(*func(a1, a2, a3, a4, a5 uintptr, a6 int64) int64)
512				return strconv.FormatInt(fn(12, 34, 56, 78, 90, math.MaxInt32+1500), 10)
513			},
514			want: strconv.FormatInt(12*1+34*2+56*3+78*4+90*5+math.MaxInt32+1500, 10),
515		},
516		{
517			// check if unaligned 64bit argument and 64bit returned value is properly passed via stack when it's occupied by floats
518			// arm-specific but must work everywhere
519			name: "arm_int64_unaligned_on_stack_after_floats",
520			fn: new(func(
521				uintptr, uintptr, uintptr, uintptr,
522				float32, float32, float32, float32,
523				float32, float32, float32, float32,
524				float32, float32, float32, float32,
525				float32, float32, float32, float32,
526				float32, int64,
527			) int64),
528			cFn: "arm_int64_unaligned_on_stack_after_floats",
529			call: func(f any) string {
530				fn := *(f).(*func(
531					a1, a2, a3, a4 uintptr,
532					f1, f2, f3, f4 float32,
533					f5, f6, f7, f8 float32,
534					f9, f10, f11, f12 float32,
535					f13, f14, f15, f16 float32,
536					f17 float32, a5 int64,
537				) int64)
538				return strconv.FormatInt(fn(
539					12, 34, 56, 78,
540					0, 0, 0, 0,
541					0, 0, 0, 0,
542					0, 0, 0, 0,
543					0, 0, 0, 0,
544					0, math.MaxInt32+1500,
545				), 10)
546			},
547			want: strconv.FormatInt(12*1+34*2+56*3+78*4+math.MaxInt32+1500, 10),
548		},
549		{
550			// check if unaligned float 64bit argument and float 64bit returned value is properly passed via registers
551			// arm-softfloat-specific but must work everywhere
552			name: "arm_float64_unaligned_in_registers",
553			fn:   new(func(uintptr, float64) float64),
554			cFn:  "arm_float64_unaligned_in_registers",
555			call: func(f any) string {
556				fn := *(f).(*func(x uintptr, y float64) float64)
557				return strconv.FormatFloat(fn(456, math.MaxFloat32+1500), 'b', 10, 64)
558			},
559			want: strconv.FormatFloat(456*123.5+math.MaxFloat32+1500, 'b', 10, 64),
560		},
561		{
562			// check if unaligned float 64bit argument and float 64bit returned value is properly passed via stack
563			// arm-softfloat-specific but must work everywhere
564			name: "arm_float64_unaligned_on_stack",
565			fn:   new(func(uintptr, uintptr, uintptr, uintptr, uintptr, float64) float64),
566			cFn:  "arm_float64_unaligned_on_stack",
567			call: func(f any) string {
568				fn := *(f).(*func(a1, a2, a3, a4, a5 uintptr, a6 float64) float64)
569				return strconv.FormatFloat(fn(12, 34, 56, 78, 90, math.MaxFloat32+1500), 'b', 10, 64)
570			},
571			want: strconv.FormatFloat(12*1+34*2+56*3+78*4+90*5+math.MaxFloat32+1500, 'b', 10, 64),
572		},
573	}
574
575	for _, tt := range tests {
576		t.Run(tt.name, func(t *testing.T) {
577			if tt.name == "20_int32" && runtime.GOARCH == "ppc64le" {
578				t.Skip("ppc64le retains the 15-argument limit")
579			}
580			if (tt.name == "10_float32" || tt.name == "arm_int64_unaligned_on_stack_after_floats") &&
581				(runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
582				t.Skip("float32 stack arguments not yet supported on this platform")
583			}
584			// Struct tests require Darwin ARM64 or AMD64
585			if strings.HasPrefix(tt.name, "8int_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
586				t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
587			}
588			if strings.HasPrefix(tt.name, "8float_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
589				t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
590			}
591
592			purego.RegisterLibFunc(tt.fn, lib, tt.cFn)
593			got := tt.call(tt.fn)
594			if got != tt.want {
595				t.Errorf("%s\n  got:  %q\n  want: %q", tt.cFn, got, tt.want)
596			}
597		})
598	}
599
600	t.Run("20_uintptr", func(t *testing.T) {
601		if runtime.GOARCH == "ppc64le" {
602			t.Skip("ppc64le retains the 15-argument limit")
603		}
604		var fn func(uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr) uintptr
605		purego.RegisterLibFunc(&fn, lib, "stack_20_uintptr")
606		got := fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
607		const want = uintptr(210)
608		if got != want {
609			t.Fatalf("stack_20_uintptr: got %d, want %d", got, want)
610		}
611	})
612
613	t.Run("32_uintptr", func(t *testing.T) {
614		if runtime.GOARCH == "ppc64le" {
615			t.Skip("ppc64le retains the 15-argument limit")
616		}
617		var fn func(
618			uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
619			uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
620			uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
621			uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
622		) uintptr
623		purego.RegisterLibFunc(&fn, lib, "stack_32_uintptr")
624		got := fn(
625			1, 2, 3, 4, 5, 6, 7, 8,
626			9, 10, 11, 12, 13, 14, 15, 16,
627			17, 18, 19, 20, 21, 22, 23, 24,
628			25, 26, 27, 28, 29, 30, 31, 32,
629		)
630		const want = uintptr(528)
631		if got != want {
632			t.Fatalf("stack_32_uintptr: got %d, want %d", got, want)
633		}
634	})
635
636	t.Run("syscalln_20_uintptr", func(t *testing.T) {
637		if runtime.GOARCH == "ppc64le" {
638			t.Skip("ppc64le retains the 15-argument limit")
639		}
640		fn, err := load.OpenSymbol(lib, "stack_20_uintptr")
641		if err != nil {
642			t.Fatalf("OpenSymbol(stack_20_uintptr) failed: %v", err)
643		}
644		got, _, _ := purego.SyscallN(fn,
645			1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
646			11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
647		)
648		const want = uintptr(210)
649		if got != want {
650			t.Fatalf("stack_20_uintptr SyscallN: got %d, want %d", got, want)
651		}
652	})
653
654	t.Run("syscalln_32_uintptr", func(t *testing.T) {
655		if runtime.GOARCH == "ppc64le" {
656			t.Skip("ppc64le retains the 15-argument limit")
657		}
658		fn, err := load.OpenSymbol(lib, "stack_32_uintptr")
659		if err != nil {
660			t.Fatalf("OpenSymbol(stack_32_uintptr) failed: %v", err)
661		}
662		got, _, _ := purego.SyscallN(fn,
663			1, 2, 3, 4, 5, 6, 7, 8,
664			9, 10, 11, 12, 13, 14, 15, 16,
665			17, 18, 19, 20, 21, 22, 23, 24,
666			25, 26, 27, 28, 29, 30, 31, 32,
667		)
668		const want = uintptr(528)
669		if got != want {
670			t.Fatalf("stack_32_uintptr SyscallN: got %d, want %d", got, want)
671		}
672	})
673
674	t.Run("32_mixed_int_float", func(t *testing.T) {
675		if unsafe.Sizeof(uintptr(0)) == 4 {
676			t.Skip("requires 64-bit uintptr slots")
677		}
678		if runtime.GOARCH == "ppc64le" {
679			t.Skip("mixed int/float stack arguments are not yet supported on ppc64le")
680		}
681
682		var fn func(
683			uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
684			uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
685			float64, float64, float64, float64, float64, float64, float64, float64,
686			float64, float64, float64, float64, float64, float64, float64, float64,
687		) float64
688		purego.RegisterLibFunc(&fn, lib, "stack_32_mixed_int_float")
689		got := fn(
690			1, 2, 3, 4, 5, 6, 7, 8,
691			9, 10, 11, 12, 13, 14, 15, 16,
692			1, 2, 3, 4, 5, 6, 7, 8,
693			9, 10, 11, 12, 13, 14, 15, 16,
694		)
695		const want = 5168.0
696		if got != want {
697			t.Fatalf("stack_32_mixed_int_float: got %f, want %f", got, want)
698		}
699	})
700}
701
702func TestABI_TooManyArguments(t *testing.T) {
703	mustPanic := func(t *testing.T, want string, f func()) {
704		t.Helper()
705		defer func() {
706			r := recover()
707			if r == nil {
708				t.Fatalf("expected panic %q, got none", want)
709			}
710			got := fmt.Sprint(r)
711			if got != want {
712				t.Fatalf("panic mismatch:\n  got:  %q\n  want: %q", got, want)
713			}
714		}()
715		f()
716	}
717
718	// 33 int64 parameters exceeds maxArgs=32.
719	t.Run("registerfunc_33_int64_exceeds_limit", func(t *testing.T) {
720		mustPanic(t, "purego: too many stack arguments", func() {
721			var fn func(
722				int64, int64, int64, int64, int64, int64, int64, int64,
723				int64, int64, int64, int64, int64, int64, int64, int64,
724				int64, int64, int64, int64, int64, int64, int64, int64,
725				int64, int64, int64, int64, int64, int64, int64, int64,
726				int64,
727			)
728			purego.RegisterFunc(&fn, 1)
729		})
730	})
731
732	t.Run("registerfunc_16_int64_exceeds_ppc64le_limit", func(t *testing.T) {
733		if runtime.GOARCH != "ppc64le" {
734			t.Skip("ppc64le retains the 15-argument limit")
735		}
736		mustPanic(t, "purego: too many stack arguments", func() {
737			var fn func(
738				int64, int64, int64, int64, int64, int64, int64, int64,
739				int64, int64, int64, int64, int64, int64, int64, int64,
740			)
741			purego.RegisterFunc(&fn, 1)
742		})
743	})
744
745	t.Run("syscalln_33_uintptr_exceeds_limit", func(t *testing.T) {
746		mustPanic(t, "purego: too many arguments to SyscallN", func() {
747			purego.SyscallN(1,
748				1, 2, 3, 4, 5, 6, 7, 8,
749				9, 10, 11, 12, 13, 14, 15, 16,
750				17, 18, 19, 20, 21, 22, 23, 24,
751				25, 26, 27, 28, 29, 30, 31, 32,
752				33,
753			)
754		})
755	})
756
757	t.Run("syscalln_16_uintptr_exceeds_ppc64le_limit", func(t *testing.T) {
758		if runtime.GOARCH != "ppc64le" {
759			t.Skip("ppc64le retains the 15-argument limit")
760		}
761		mustPanic(t, "purego: too many arguments to SyscallN", func() {
762			purego.SyscallN(1,
763				1, 2, 3, 4, 5, 6, 7, 8,
764				9, 10, 11, 12, 13, 14, 15, 16,
765			)
766		})
767	})
768}
769
770func TestABI_StructReturnHiddenPointer(t *testing.T) {
771	// A struct returned in memory is passed a hidden pointer as the first
772	// integer argument, so maxArgs integer arguments plus it need one more slot
773	// than sysargs holds and RegisterFunc must reject the registration.
774	type bigStruct struct{ A, B, C uint64 } // larger than two eightbytes
775
776	if !purego.StructReturnInMemory(reflect.TypeFor[bigStruct]()) {
777		t.Skipf("GOARCH=%s does not return large structs via a hidden integer argument", runtime.GOARCH)
778	}
779
780	in := make([]reflect.Type, purego.MaxArgs)
781	for i := range in {
782		in[i] = reflect.TypeFor[uintptr]()
783	}
784	fnType := reflect.FuncOf(in, []reflect.Type{reflect.TypeFor[bigStruct]()}, false)
785	fptr := reflect.New(fnType)
786
787	defer func() {
788		switch r := recover(); {
789		case r == nil:
790			t.Fatal("RegisterFunc accepted the call; the hidden struct-return pointer was not counted against the stack limit")
791		case strings.Contains(fmt.Sprint(r), "too many stack arguments"):
792			// Expected: the guard fired.
793		case strings.Contains(fmt.Sprint(r), "only supported on"):
794			t.Skipf("struct returns are unsupported on this platform: %v", r)
795		default:
796			t.Fatalf("unexpected panic: %v", r)
797		}
798	}()
799
800	// A non-zero cfn passes the nil check; the panic fires during the preflight
801	// argument count, before the function is ever called.
802	purego.RegisterFunc(fptr.Interface(), uintptr(1))
803}
804
805func buildSharedLib(tb testing.TB, compilerEnv, libFile string, sources ...string) error {
806	tb.Helper()
807	// When PUREGO_TEST_PREBUILT_LIBDIR is set, the shared library has been
808	// cross-compiled ahead of time and placed in that directory under the
809	// base name of libFile. This allows running the tests on a target that
810	// has no C toolchain, such as an Android emulator.
811	if dir := os.Getenv("PUREGO_TEST_PREBUILT_LIBDIR"); dir != "" {
812		data, err := os.ReadFile(filepath.Join(dir, filepath.Base(libFile)))
813		if err != nil {
814			return fmt.Errorf("prebuilt lib: %w", err)
815		}
816		if err := os.WriteFile(libFile, data, 0o755); err != nil {
817			return fmt.Errorf("prebuilt lib: %w", err)
818		}
819		return nil
820	}
821
822	// Compiling the library needs a C toolchain targeting GOARCH. CI has none
823	// for Windows on 386 or arm64, so skip those (the prebuilt path above
824	// avoids the toolchain).
825	if runtime.GOOS == "windows" {
826		switch runtime.GOARCH {
827		case "386":
828			tb.Skip("need a 386 C toolchain to run this test") // TODO: find a 386 C toolchain for test
829		case "arm64":
830			tb.Skip("need an arm64 C toolchain to run this test")
831		}
832	}
833
834	out, err := exec.Command("go", "env", compilerEnv).Output()
835	if err != nil {
836		return fmt.Errorf("go env %s error: %w", compilerEnv, err)
837	}
838
839	compiler := strings.TrimSpace(string(out))
840	if compiler == "" {
841		return errors.New("compiler not found")
842	}
843
844	args := []string{"-shared", "-Wall", "-Werror", "-fPIC", "-o", libFile}
845	if runtime.GOARCH == "386" {
846		args = append(args, "-m32")
847	}
848	// macOS arm64 can run amd64 tests through Rossetta.
849	// Build the shared library based on the GOARCH and not
850	// the default behavior of the compiler.
851	if runtime.GOOS == "darwin" {
852		var arch string
853		switch runtime.GOARCH {
854		case "arm64":
855			arch = "arm64"
856		case "amd64":
857			arch = "x86_64"
858		default:
859			return fmt.Errorf("unknown macOS architecture %s", runtime.GOARCH)
860		}
861		args = append(args, "-arch", arch)
862	}
863	cmd := exec.Command(compiler, append(args, sources...)...)
864	if out, err := cmd.CombinedOutput(); err != nil {
865		return fmt.Errorf("compile lib: %w\n%q\n%s", err, cmd, string(out))
866	}
867
868	return nil
869}

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