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