v0.11.0 11ec78d

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

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