1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3
4//go:build darwin || freebsd || linux || netbsd || windows
5
6package purego
7
8import (
9 "fmt"
10 "math"
11 "reflect"
12 "runtime"
13 "unsafe"
14
15 "github.com/ebitengine/purego/internal/strings"
16 "github.com/ebitengine/purego/internal/xreflect"
17)
18
19const (
20 align8ByteMask = 7 // Mask for 8-byte alignment: (val + 7) &^ 7
21 align8ByteSize = 8 // 8-byte alignment boundary
22)
23
24// RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name).
25// It panics if it can't find the name symbol.
26func RegisterLibFunc(fptr any, handle uintptr, name string) {
27 sym, err := loadSymbol(handle, name)
28 if err != nil {
29 panic(err)
30 }
31 RegisterFunc(fptr, sym)
32}
33
34// RegisterFunc takes a pointer to a Go function representing the calling convention of the C function.
35// fptr will be set to a function that when called will call the C function given by cfn with the
36// parameters passed in the correct registers and stack.
37//
38// A panic is produced if the type is not a function pointer or if the function returns more than 1 value.
39//
40// These conversions describe how a Go type in the fptr will be used to call
41// the C function. It is important to note that there is no way to verify that fptr
42// matches the C function. This also holds true for struct types where the padding
43// needs to be ensured to match that of C; RegisterFunc does not verify this.
44//
45// # Type Conversions (Go <=> C)
46//
47// string <=> char*
48// bool <=> _Bool
49// uintptr <=> uintptr_t
50// uint <=> uint32_t or uint64_t
51// uint8 <=> uint8_t
52// uint16 <=> uint16_t
53// uint32 <=> uint32_t
54// uint64 <=> uint64_t
55// int <=> int32_t or int64_t
56// int8 <=> int8_t
57// int16 <=> int16_t
58// int32 <=> int32_t
59// int64 <=> int64_t
60// float32 <=> float
61// float64 <=> double
62// struct <=> struct (darwin amd64/arm64, linux amd64/arm64)
63// func <=> C function
64// unsafe.Pointer, *T <=> void*
65// []T => void*
66//
67// There is a special case when the last argument of fptr is a variadic interface (or []interface}
68// it will be expanded into a call to the C function as if it had the arguments in that slice.
69// This means that using arg ...any is like a cast to the function with the arguments inside arg.
70// This is not the same as C variadic.
71//
72// # Memory
73//
74// In general it is not possible for purego to guarantee the lifetimes of objects returned or received from
75// calling functions using RegisterFunc. For arguments to a C function it is important that the C function doesn't
76// hold onto a reference to Go memory. This is the same as the [Cgo rules].
77//
78// However, there are some special cases. When passing a string as an argument if the string does not end in a null
79// terminated byte (\x00) then the string will be copied into memory maintained by purego. The memory is only valid for
80// that specific call. Therefore, if the C code keeps a reference to that string it may become invalid at some
81// undefined time. However, if the string does already contain a null-terminated byte then no copy is done.
82// It is then the responsibility of the caller to ensure the string stays alive as long as it's needed in C memory.
83// This can be done using runtime.KeepAlive or allocating the string in C memory using malloc. When a C function
84// returns a null-terminated pointer to char a Go string can be used. Purego will allocate a new string in Go memory
85// and copy the data over. This string will be garbage collected whenever Go decides it's no longer referenced.
86// This C created string will not be freed by purego. If the pointer to char is not null-terminated or must continue
87// to point to C memory (because it's a buffer for example) then use a pointer to byte and then convert that to a slice
88// using unsafe.Slice. Doing this means that it becomes the responsibility of the caller to care about the lifetime
89// of the pointer
90//
91// # Structs
92//
93// Purego can handle the most common structs that have fields of builtin types like int8, uint16, float32, etc. However,
94// it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
95// that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
96//
97// On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack,
98// following the C ABI's byte-level packing rules.
99//
100// # Example
101//
102// All functions below call this C function:
103//
104// char *foo(char *str);
105//
106// // Let purego convert types
107// var foo func(s string) string
108// goString := foo("copied")
109// // Go will garbage collect this string
110//
111// // Manually, handle allocations
112// var foo2 func(b string) *byte
113// mustFree := foo2("not copied\x00")
114// defer free(mustFree)
115//
116// [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
117func RegisterFunc(fptr any, cfn uintptr) {
118 const is32bit = unsafe.Sizeof(uintptr(0)) == 4
119 fn := reflect.ValueOf(fptr).Elem()
120 ty := fn.Type()
121 if ty.Kind() != reflect.Func {
122 panic("purego: fptr must be a function pointer")
123 }
124 if ty.NumOut() > 1 {
125 panic("purego: function can only return zero or one values")
126 }
127 if cfn == 0 {
128 panic("purego: cfn is nil")
129 }
130 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
131 runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
132 panic("purego: float returns are not supported")
133 }
134 {
135 // this code checks how many registers and stack this function will use
136 // to avoid crashing with too many arguments
137 var ints int
138 var floats int
139 var stack int
140 for i := 0; i < ty.NumIn(); i++ {
141 arg := ty.In(i)
142 switch arg.Kind() {
143 case reflect.Func:
144 // This only does preliminary testing to ensure the CDecl argument
145 // is the first argument. Full testing is done when the callback is actually
146 // created in NewCallback.
147 for j := 0; j < arg.NumIn(); j++ {
148 in := arg.In(j)
149 if !in.AssignableTo(reflect.TypeOf(CDecl{})) {
150 continue
151 }
152 if j != 0 {
153 panic("purego: CDecl must be the first argument")
154 }
155 }
156 case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
157 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Ptr, reflect.UnsafePointer,
158 reflect.Slice, reflect.Bool:
159 if ints < numOfIntegerRegisters() {
160 ints++
161 } else {
162 stack++
163 }
164 case reflect.Float32, reflect.Float64:
165 if floats < numOfFloatRegisters() {
166 floats++
167 } else {
168 stack++
169 }
170 case reflect.Struct:
171 ensureStructSupportedForRegisterFunc()
172 if arg.Size() == 0 {
173 continue
174 }
175 addInt := func(u uintptr) {
176 ints++
177 }
178 addFloat := func(u uintptr) {
179 floats++
180 }
181 addStack := func(u uintptr) {
182 stack++
183 }
184 _ = addStruct(reflect.New(arg).Elem(), &ints, &floats, &stack, addInt, addFloat, addStack, nil)
185 default:
186 panic("purego: unsupported kind " + arg.Kind().String())
187 }
188 }
189 if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
190 ensureStructSupportedForRegisterFunc()
191 outType := ty.Out(0)
192 checkStructFieldsSupported(outType)
193 if runtime.GOARCH == "amd64" && outType.Size() > maxRegAllocStructSize {
194 // on amd64 if struct is bigger than 16 bytes allocate the return struct
195 // and pass it in as a hidden first argument.
196 ints++
197 }
198 }
199
200 sizeOfStack := maxArgs - numOfIntegerRegisters()
201 // On Darwin ARM64, use byte-based validation since arguments pack efficiently.
202 // See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
203 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
204 stackBytes := estimateStackBytes(ty)
205 maxStackBytes := sizeOfStack * 8
206 if stackBytes > maxStackBytes {
207 panic("purego: too many stack arguments")
208 }
209 } else {
210 if stack > sizeOfStack {
211 panic("purego: too many stack arguments")
212 }
213 }
214 }
215
216 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
217 var sysargs [maxArgs]uintptr
218 // Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free,
219 // since numOfFloatRegisters() is a function call, not a constant.
220 // maxArgs is always greater than or equal to numOfFloatRegisters() so this is safe.
221 var floats [maxArgs]uintptr
222 var numInts int
223 var numFloats int
224 var numStack int
225 var addStack, addInt, addFloat func(x uintptr)
226 if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
227 // Windows arm64 uses the same calling convention as macOS and Linux
228 addStack = func(x uintptr) {
229 sysargs[numOfIntegerRegisters()+numStack] = x
230 numStack++
231 }
232 addInt = func(x uintptr) {
233 if numInts >= numOfIntegerRegisters() {
234 addStack(x)
235 } else {
236 sysargs[numInts] = x
237 numInts++
238 }
239 }
240 addFloat = func(x uintptr) {
241 if numFloats < numOfFloatRegisters() {
242 floats[numFloats] = x
243 numFloats++
244 } else {
245 addStack(x)
246 }
247 }
248 } else {
249 // On Windows amd64 the arguments are passed in the numbered registered.
250 // So the first int is in the first integer register and the first float
251 // is in the second floating register if there is already a first int.
252 // This is in contrast to how macOS and Linux pass arguments which
253 // tries to use as many registers as possible in the calling convention.
254 addStack = func(x uintptr) {
255 sysargs[numStack] = x
256 numStack++
257 }
258 addInt = addStack
259 addFloat = addStack
260 }
261
262 var keepAlive []any
263 defer func() {
264 runtime.KeepAlive(keepAlive)
265 runtime.KeepAlive(args)
266 }()
267
268 var arm64_r8 uintptr
269 if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
270 outType := ty.Out(0)
271 if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
272 val := reflect.New(outType)
273 keepAlive = append(keepAlive, val)
274 addInt(val.Pointer())
275 } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
276 isAllFloats, numFields := isAllSameFloat(outType)
277 if !isAllFloats || numFields > 4 {
278 val := reflect.New(outType)
279 keepAlive = append(keepAlive, val)
280 arm64_r8 = val.Pointer()
281 }
282 }
283 }
284 for i, v := range args {
285 if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok {
286 if i != len(args)-1 {
287 panic("purego: can only expand last parameter")
288 }
289 for _, x := range variadic {
290 keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
291 }
292 continue
293 }
294 // Check if we need to start Darwin ARM64 C-style stack packing
295 if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
296 // Collect and separate remaining args into register vs stack
297 stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
298 keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
299 keepAlive = newKeepAlive
300
301 // Bundle stack arguments with C-style packing
302 bundleStackArgs(stackArgs, addStack)
303 break
304 }
305 keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
306 }
307
308 syscall := &syscall15Args{}
309
310 if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
311 syscall.Set(cfn, sysargs[:], floats[:], 0)
312 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
313 } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
314 // Use the normal arm64 calling convention even on Windows
315 syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
316 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
317 } else {
318 // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats
319 syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4],
320 sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
321 sysargs[12], sysargs[13], sysargs[14])
322 syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
323 }
324 if ty.NumOut() == 0 {
325 return nil
326 }
327 outType := ty.Out(0)
328 v := reflect.New(outType).Elem()
329 switch outType.Kind() {
330 case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
331 v.SetUint(uint64(syscall.a1))
332 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
333 v.SetInt(int64(syscall.a1))
334 case reflect.Bool:
335 v.SetBool(byte(syscall.a1) != 0)
336 case reflect.UnsafePointer:
337 // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
338 v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
339 case reflect.Ptr:
340 v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem()
341 case reflect.Func:
342 // wrap this C function in a nicely typed Go function
343 v = reflect.New(outType)
344 RegisterFunc(v.Interface(), syscall.a1)
345 case reflect.String:
346 v.SetString(strings.GoString(syscall.a1))
347 case reflect.Float32:
348 // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
349 // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
350 // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
351 // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
352 // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
353 switch runtime.GOARCH {
354 case "386":
355 v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
356 case "ppc64le":
357 v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
358 case "s390x":
359 // S390X is big-endian: float32 in upper 32 bits of 64-bit register
360 v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
361 default:
362 v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
363 }
364 case reflect.Float64:
365 // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
366 // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
367 if is32bit {
368 v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
369 } else {
370 v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
371 }
372 case reflect.Struct:
373 v = getStruct(outType, *syscall)
374 default:
375 panic("purego: unsupported return kind: " + outType.Kind().String())
376 }
377 if len(args) > 0 {
378 // reuse args slice instead of allocating one when possible
379 args[0] = v
380 return args[:1]
381 } else {
382 return []reflect.Value{v}
383 }
384 })
385 fn.Set(v)
386}
387
388func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {
389 const is32bit = unsafe.Sizeof(uintptr(0)) == 4
390 switch v.Kind() {
391 case reflect.String:
392 ptr := strings.CString(v.String())
393 keepAlive = append(keepAlive, ptr)
394 addInt(uintptr(unsafe.Pointer(ptr)))
395 case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
396 addInt(uintptr(v.Uint()))
397 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
398 addInt(uintptr(v.Int()))
399 case reflect.Ptr, reflect.UnsafePointer, reflect.Slice:
400 // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
401 addInt(v.Pointer())
402 case reflect.Func:
403 addInt(NewCallback(v.Interface()))
404 case reflect.Bool:
405 if v.Bool() {
406 addInt(1)
407 } else {
408 addInt(0)
409 }
410 case reflect.Float32:
411 // On S390X big-endian, float32 goes in upper 32 bits of 64-bit FP register
412 if runtime.GOARCH == "s390x" {
413 addFloat(uintptr(math.Float32bits(float32(v.Float()))) << 32)
414 } else {
415 addFloat(uintptr(math.Float32bits(float32(v.Float()))))
416 }
417 case reflect.Float64:
418 if is32bit {
419 bits := math.Float64bits(v.Float())
420 addFloat(uintptr(bits))
421 addFloat(uintptr(bits >> 32))
422 } else {
423 addFloat(uintptr(math.Float64bits(v.Float())))
424 }
425 case reflect.Struct:
426 keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
427 default:
428 panic("purego: unsupported kind: " + v.Kind().String())
429 }
430 return keepAlive
431}
432
433// maxRegAllocStructSize is the biggest a struct can be while still fitting in registers.
434// if it is bigger than this than enough space must be allocated on the heap and then passed into
435// the function as the first parameter on amd64 or in R8 on arm64.
436//
437// If you change this make sure to update it in objc_runtime_darwin.go
438const maxRegAllocStructSize = 16
439
440func isAllSameFloat(ty reflect.Type) (allFloats bool, numFields int) {
441 allFloats = true
442 root := ty.Field(0).Type
443 for root.Kind() == reflect.Struct {
444 root = root.Field(0).Type
445 }
446 first := root.Kind()
447 if first != reflect.Float32 && first != reflect.Float64 {
448 allFloats = false
449 }
450 for i := 0; i < ty.NumField(); i++ {
451 f := ty.Field(i).Type
452 if f.Kind() == reflect.Struct {
453 var structNumFields int
454 allFloats, structNumFields = isAllSameFloat(f)
455 numFields += structNumFields
456 continue
457 }
458 numFields++
459 if f.Kind() != first {
460 allFloats = false
461 }
462 }
463 return allFloats, numFields
464}
465
466func checkStructFieldsSupported(ty reflect.Type) {
467 for i := 0; i < ty.NumField(); i++ {
468 f := ty.Field(i).Type
469 if f.Kind() == reflect.Array {
470 f = f.Elem()
471 } else if f.Kind() == reflect.Struct {
472 checkStructFieldsSupported(f)
473 continue
474 }
475 switch f.Kind() {
476 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
477 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
478 reflect.Uintptr, reflect.Ptr, reflect.UnsafePointer, reflect.Float64, reflect.Float32,
479 reflect.Bool:
480 default:
481 panic(fmt.Sprintf("purego: struct field type %s is not supported", f))
482 }
483 }
484}
485
486func ensureStructSupportedForRegisterFunc() {
487 if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
488 panic("purego: struct arguments are only supported on amd64 and arm64")
489 }
490 if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
491 panic("purego: struct arguments are only supported on darwin and linux")
492 }
493}
494
495func roundUpTo8(val uintptr) uintptr {
496 return (val + align8ByteMask) &^ align8ByteMask
497}
498
499func numOfFloatRegisters() int {
500 switch runtime.GOARCH {
501 case "amd64", "arm64", "loong64", "ppc64le", "riscv64":
502 return 8
503 case "s390x":
504 return 4
505 case "arm":
506 return 16
507 case "386":
508 // i386 SysV ABI passes all arguments on the stack, including floats
509 return 0
510 default:
511 // since this platform isn't supported and can therefore only access
512 // integer registers it is safest to return 8
513 return 8
514 }
515}
516
517func numOfIntegerRegisters() int {
518 switch runtime.GOARCH {
519 case "arm64", "loong64", "ppc64le", "riscv64":
520 return 8
521 case "amd64":
522 return 6
523 case "s390x":
524 // S390X uses R2-R6 for integer arguments
525 return 5
526 case "arm":
527 return 4
528 case "386":
529 // i386 SysV ABI passes all arguments on the stack
530 return 0
531 default:
532 // since this platform isn't supported and can therefore only access
533 // integer registers it is fine to return the maxArgs
534 return maxArgs
535 }
536}
537
538// estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.
539// This is a conservative estimate used only for early error detection.
540func estimateStackBytes(ty reflect.Type) int {
541 var numInts, numFloats int
542 var stackBytes int
543
544 for i := 0; i < ty.NumIn(); i++ {
545 arg := ty.In(i)
546 size := int(arg.Size())
547
548 // Check if this goes to register or stack
549 usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64
550 if usesInt && numInts < numOfIntegerRegisters() {
551 numInts++
552 } else if !usesInt && numFloats < numOfFloatRegisters() {
553 numFloats++
554 } else {
555 // Goes to stack - accumulate total bytes
556 stackBytes += size
557 }
558 }
559 // Round total to 8-byte boundary
560 if stackBytes > 0 && stackBytes%align8ByteSize != 0 {
561 stackBytes = int(roundUpTo8(uintptr(stackBytes)))
562 }
563 return stackBytes
564}