purego: fix Darwin ARM64 calling convention for struct arguments, callbacks, and stack packing (#353)
Darwin ARM64 uses byte-aligned argument packing, not 8-byte slots. The current code uses 8-byte slots for everything, causing two problems: 1. Functions with >8 parameters fail when using int32, float32, etc. 2. Struct arguments are packed incorrectly. For example: ``` // C: int32_t sum(int32_t a1, ..., int32_t a11) var fn func(int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32) int32 purego.RegisterLibFunc(&fn, lib, "sum_11_int32") result := fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) // fails: stack arguments misaligned ``` The fix implements proper Darwin ARM64 calling conventions: * Stack arguments use natural alignment (int32 = 4 bytes, not 8). * Multiple small values pack into single 8-byte slots. * Struct arguments use byte-level copying, not slot alignment. * Callbacks unpack arguments using Darwin-specific rules. * Register allocation tries registers before falling back to stack. The code splits ARM64 struct handling into darwin and non-darwin versions to preserve correct behavior on Linux and other ARM64 systems. Closes #352 --------- Co-authored-by: TotallyGamerJet <TotallyGamerJet@users.noreply.github.com>
- commit
- 6b665882fe54db38661e06d0dc959ea695db6030
- author
- Travis Cline · · 2025-12-31 18:16 UTC
- committer
- GitHub · · 2025-12-31 18:16 UTC
- parent
- f6151bf
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
12 files · +866 -49
M func.go +97 -25
| @@ -10,7 +10,6 @@ import ( | |||
| 10 | 10 | "math" | |
| 11 | 11 | "reflect" | |
| 12 | 12 | "runtime" | |
| 13 | - | "strconv" | |
| 14 | 13 | "sync" | |
| 15 | 14 | "unsafe" | |
| 16 | 15 | ||
| @@ -18,6 +17,11 @@ import ( | |||
| 18 | 17 | "github.com/ebitengine/purego/internal/xreflect" | |
| 19 | 18 | ) | |
| 20 | 19 | ||
| 20 | + | const ( | |
| 21 | + | align8ByteMask = 7 // Mask for 8-byte alignment: (val + 7) &^ 7 | |
| 22 | + | align8ByteSize = 8 // 8-byte alignment boundary | |
| 23 | + | ) | |
| 24 | + | ||
| 21 | 25 | var thePool = sync.Pool{New: func() any { | |
| 22 | 26 | return new(syscall15Args) | |
| 23 | 27 | }} | |
| @@ -95,6 +99,9 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { | |||
| 95 | 99 | // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure | |
| 96 | 100 | // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example. | |
| 97 | 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 | + | // | |
| 98 | 105 | // # Example | |
| 99 | 106 | // | |
| 100 | 107 | // All functions below call this C function: | |
| @@ -201,11 +208,27 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||
| 201 | 208 | ints++ | |
| 202 | 209 | } | |
| 203 | 210 | } | |
| 211 | + | ||
| 204 | 212 | sizeOfStack := maxArgs - numOfIntegerRegisters() | |
| 205 | - | if stack > sizeOfStack { | |
| 206 | - | panic("purego: too many arguments") | |
| 213 | + | // On Darwin ARM64, use byte-based validation since arguments pack efficiently | |
| 214 | + | if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { | |
| 215 | + | stackBytes := estimateStackBytes(ty) | |
| 216 | + | maxStackBytes := sizeOfStack * 8 | |
| 217 | + | if stackBytes > maxStackBytes { | |
| 218 | + | panic("purego: too many stack arguments") | |
| 219 | + | } | |
| 220 | + | } else { | |
| 221 | + | if stack > sizeOfStack { | |
| 222 | + | panic("purego: too many stack arguments") | |
| 223 | + | } | |
| 207 | 224 | } | |
| 208 | 225 | } | |
| 226 | + | ||
| 227 | + | // Detect if cfn is a callback (to avoid tight packing for callbacks which still use 8-byte slots) | |
| 228 | + | // TODO: Remove this check once Darwin ARM64 callback unpacking is updated to handle C-style tight packing. | |
| 229 | + | // When callbacks can unpack tightly-packed arguments, this workaround can be removed. | |
| 230 | + | isCallback := isCallbackFunction(cfn) | |
| 231 | + | ||
| 209 | 232 | v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | |
| 210 | 233 | var sysargs [maxArgs]uintptr | |
| 211 | 234 | var floats [numOfFloatRegisters]uintptr | |
| @@ -281,28 +304,17 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||
| 281 | 304 | } | |
| 282 | 305 | continue | |
| 283 | 306 | } | |
| 284 | - | if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && | |
| 285 | - | (numInts >= numOfIntegerRegisters() || numFloats >= numOfFloatRegisters) && v.Kind() != reflect.Struct { // hit the stack | |
| 286 | - | fields := make([]reflect.StructField, len(args[i:])) | |
| 307 | + | // Check if we need to start Darwin ARM64 C-style stack packing | |
| 308 | + | // Skip tight packing for callbacks since they still use 8-byte slot unpacking | |
| 309 | + | // TODO: Remove !isCallback condition once callback unpacking supports tight packing | |
| 310 | + | if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && !isCallback && shouldBundleStackArgs(v, numInts, numFloats) { | |
| 311 | + | // Collect and separate remaining args into register vs stack | |
| 312 | + | stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats, | |
| 313 | + | keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 314 | + | keepAlive = newKeepAlive | |
| 287 | 315 | ||
| 288 | - | for j, val := range args[i:] { | |
| 289 | - | if val.Kind() == reflect.String { | |
| 290 | - | ptr := strings.CString(val.String()) | |
| 291 | - | keepAlive = append(keepAlive, ptr) | |
| 292 | - | val = reflect.ValueOf(ptr) | |
| 293 | - | args[i+j] = val | |
| 294 | - | } | |
| 295 | - | fields[j] = reflect.StructField{ | |
| 296 | - | Name: "X" + strconv.Itoa(j), | |
| 297 | - | Type: val.Type(), | |
| 298 | - | } | |
| 299 | - | } | |
| 300 | - | structType := reflect.StructOf(fields) | |
| 301 | - | structInstance := reflect.New(structType).Elem() | |
| 302 | - | for j, val := range args[i:] { | |
| 303 | - | structInstance.Field(j).Set(val) | |
| 304 | - | } | |
| 305 | - | placeRegisters(structInstance, addFloat, addInt) | |
| 316 | + | // Bundle stack arguments with C-style packing | |
| 317 | + | bundleStackArgs(stackArgs, addStack) | |
| 306 | 318 | break | |
| 307 | 319 | } | |
| 308 | 320 | keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| @@ -473,7 +485,7 @@ func checkStructFieldsSupported(ty reflect.Type) { | |||
| 473 | 485 | } | |
| 474 | 486 | ||
| 475 | 487 | func roundUpTo8(val uintptr) uintptr { | |
| 476 | - | return (val + 7) &^ 7 | |
| 488 | + | return (val + align8ByteMask) &^ align8ByteMask | |
| 477 | 489 | } | |
| 478 | 490 | ||
| 479 | 491 | func numOfIntegerRegisters() int { | |
| @@ -488,3 +500,63 @@ func numOfIntegerRegisters() int { | |||
| 488 | 500 | return maxArgs | |
| 489 | 501 | } | |
| 490 | 502 | } | |
| 503 | + | ||
| 504 | + | // estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation. | |
| 505 | + | // This is a conservative estimate used only for early error detection. | |
| 506 | + | func estimateStackBytes(ty reflect.Type) int { | |
| 507 | + | numInts, numFloats := 0, 0 | |
| 508 | + | stackBytes := 0 | |
| 509 | + | ||
| 510 | + | for i := 0; i < ty.NumIn(); i++ { | |
| 511 | + | arg := ty.In(i) | |
| 512 | + | size := int(arg.Size()) | |
| 513 | + | ||
| 514 | + | // Check if this goes to register or stack | |
| 515 | + | usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64 | |
| 516 | + | if usesInt && numInts < numOfIntegerRegisters() { | |
| 517 | + | numInts++ | |
| 518 | + | } else if !usesInt && numFloats < numOfFloatRegisters { | |
| 519 | + | numFloats++ | |
| 520 | + | } else { | |
| 521 | + | // Goes to stack - accumulate total bytes | |
| 522 | + | stackBytes += size | |
| 523 | + | } | |
| 524 | + | } | |
| 525 | + | // Round total to 8-byte boundary | |
| 526 | + | if stackBytes > 0 && stackBytes%align8ByteSize != 0 { | |
| 527 | + | stackBytes = int(roundUpTo8(uintptr(stackBytes))) | |
| 528 | + | } | |
| 529 | + | return stackBytes | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | // isCallbackFunction checks if the given function pointer is a purego callback. | |
| 533 | + | // We need to detect this to avoid using tight packing for callbacks, since callback | |
| 534 | + | // unpacking still uses the 8-byte slot convention. | |
| 535 | + | // TODO: This function can be removed once Darwin ARM64 callbacks support tight packing. | |
| 536 | + | // Once callbackWrap is updated to unpack C-style arguments, callbacks can use the same | |
| 537 | + | // tight packing as normal C function calls. | |
| 538 | + | func isCallbackFunction(cfn uintptr) bool { | |
| 539 | + | // Only platforms with syscall_sysv.go have callback detection. | |
| 540 | + | // Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd | |
| 541 | + | hasSyscallSysv := runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" || | |
| 542 | + | (runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64")) | |
| 543 | + | if !hasSyscallSysv { | |
| 544 | + | return false | |
| 545 | + | } | |
| 546 | + | ||
| 547 | + | // Determine callback entry size based on architecture | |
| 548 | + | var entrySize int | |
| 549 | + | switch runtime.GOARCH { | |
| 550 | + | case "386", "amd64": | |
| 551 | + | entrySize = 5 | |
| 552 | + | case "arm", "arm64", "loong64": | |
| 553 | + | entrySize = 8 | |
| 554 | + | default: | |
| 555 | + | return false | |
| 556 | + | } | |
| 557 | + | ||
| 558 | + | // Check if cfn is in the callback address range | |
| 559 | + | callbackStart := getCallbackStart() | |
| 560 | + | callbackEnd := callbackStart + uintptr(getMaxCB()*entrySize) | |
| 561 | + | return cfn >= callbackStart && cfn < callbackEnd | |
| 562 | + | } | |
| @@ -10,7 +10,6 @@ import ( | |||||
| 10 | "math" | 10 | "math" | ||
| 11 | "reflect" | 11 | "reflect" | ||
| 12 | "runtime" | 12 | "runtime" | ||
| 13 | - | "strconv" | |||
| 14 | "sync" | 13 | "sync" | ||
| 15 | "unsafe" | 14 | "unsafe" | ||
| 16 | 15 | ||||
| @@ -18,6 +17,11 @@ import ( | |||||
| 18 | "github.com/ebitengine/purego/internal/xreflect" | 17 | "github.com/ebitengine/purego/internal/xreflect" | ||
| 19 | ) | 18 | ) | ||
| 20 | 19 | ||||
| 20 | + | const ( | |||
| 21 | + | align8ByteMask = 7 // Mask for 8-byte alignment: (val + 7) &^ 7 | |||
| 22 | + | align8ByteSize = 8 // 8-byte alignment boundary | |||
| 23 | + | ) | |||
| 24 | + | ||||
| 21 | var thePool = sync.Pool{New: func() any { | 25 | var thePool = sync.Pool{New: func() any { | ||
| 22 | return new(syscall15Args) | 26 | return new(syscall15Args) | ||
| 23 | }} | 27 | }} | ||
| @@ -95,6 +99,9 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { | |||||
| 95 | // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure | 99 | // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure | ||
| 96 | // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example. | 100 | // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example. | ||
| 97 | // | 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 | + | // | |||
| 98 | // # Example | 105 | // # Example | ||
| 99 | // | 106 | // | ||
| 100 | // All functions below call this C function: | 107 | // All functions below call this C function: | ||
| @@ -201,11 +208,27 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||||
| 201 | ints++ | 208 | ints++ | ||
| 202 | } | 209 | } | ||
| 203 | } | 210 | } | ||
| 211 | + | ||||
| 204 | sizeOfStack := maxArgs - numOfIntegerRegisters() | 212 | sizeOfStack := maxArgs - numOfIntegerRegisters() | ||
| 205 | - | if stack > sizeOfStack { | 213 | + | // On Darwin ARM64, use byte-based validation since arguments pack efficiently |
| 206 | - | panic("purego: too many arguments") | 214 | + | if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { |
| 215 | + | stackBytes := estimateStackBytes(ty) | |||
| 216 | + | maxStackBytes := sizeOfStack * 8 | |||
| 217 | + | if stackBytes > maxStackBytes { | |||
| 218 | + | panic("purego: too many stack arguments") | |||
| 219 | + | } | |||
| 220 | + | } else { | |||
| 221 | + | if stack > sizeOfStack { | |||
| 222 | + | panic("purego: too many stack arguments") | |||
| 223 | + | } | |||
| 207 | } | 224 | } | ||
| 208 | } | 225 | } | ||
| 226 | + | ||||
| 227 | + | // Detect if cfn is a callback (to avoid tight packing for callbacks which still use 8-byte slots) | |||
| 228 | + | // TODO: Remove this check once Darwin ARM64 callback unpacking is updated to handle C-style tight packing. | |||
| 229 | + | // When callbacks can unpack tightly-packed arguments, this workaround can be removed. | |||
| 230 | + | isCallback := isCallbackFunction(cfn) | |||
| 231 | + | ||||
| 209 | v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | 232 | v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | ||
| 210 | var sysargs [maxArgs]uintptr | 233 | var sysargs [maxArgs]uintptr | ||
| 211 | var floats [numOfFloatRegisters]uintptr | 234 | var floats [numOfFloatRegisters]uintptr | ||
| @@ -281,28 +304,17 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||||
| 281 | } | 304 | } | ||
| 282 | continue | 305 | continue | ||
| 283 | } | 306 | } | ||
| 284 | - | if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && | 307 | + | // Check if we need to start Darwin ARM64 C-style stack packing |
| 285 | - | (numInts >= numOfIntegerRegisters() || numFloats >= numOfFloatRegisters) && v.Kind() != reflect.Struct { // hit the stack | 308 | + | // Skip tight packing for callbacks since they still use 8-byte slot unpacking |
| 286 | - | fields := make([]reflect.StructField, len(args[i:])) | 309 | + | // TODO: Remove !isCallback condition once callback unpacking supports tight packing |
| 310 | + | if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && !isCallback && shouldBundleStackArgs(v, numInts, numFloats) { | |||
| 311 | + | // Collect and separate remaining args into register vs stack | |||
| 312 | + | stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats, | |||
| 313 | + | keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |||
| 314 | + | keepAlive = newKeepAlive | |||
| 287 | 315 | ||||
| 288 | - | for j, val := range args[i:] { | 316 | + | // Bundle stack arguments with C-style packing |
| 289 | - | if val.Kind() == reflect.String { | 317 | + | bundleStackArgs(stackArgs, addStack) |
| 290 | - | ptr := strings.CString(val.String()) | |||
| 291 | - | keepAlive = append(keepAlive, ptr) | |||
| 292 | - | val = reflect.ValueOf(ptr) | |||
| 293 | - | args[i+j] = val | |||
| 294 | - | } | |||
| 295 | - | fields[j] = reflect.StructField{ | |||
| 296 | - | Name: "X" + strconv.Itoa(j), | |||
| 297 | - | Type: val.Type(), | |||
| 298 | - | } | |||
| 299 | - | } | |||
| 300 | - | structType := reflect.StructOf(fields) | |||
| 301 | - | structInstance := reflect.New(structType).Elem() | |||
| 302 | - | for j, val := range args[i:] { | |||
| 303 | - | structInstance.Field(j).Set(val) | |||
| 304 | - | } | |||
| 305 | - | placeRegisters(structInstance, addFloat, addInt) | |||
| 306 | break | 318 | break | ||
| 307 | } | 319 | } | ||
| 308 | keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | 320 | keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | ||
| @@ -473,7 +485,7 @@ func checkStructFieldsSupported(ty reflect.Type) { | |||||
| 473 | } | 485 | } | ||
| 474 | 486 | ||||
| 475 | func roundUpTo8(val uintptr) uintptr { | 487 | func roundUpTo8(val uintptr) uintptr { | ||
| 476 | - | return (val + 7) &^ 7 | 488 | + | return (val + align8ByteMask) &^ align8ByteMask |
| 477 | } | 489 | } | ||
| 478 | 490 | ||||
| 479 | func numOfIntegerRegisters() int { | 491 | func numOfIntegerRegisters() int { | ||
| @@ -488,3 +500,63 @@ func numOfIntegerRegisters() int { | |||||
| 488 | return maxArgs | 500 | return maxArgs | ||
| 489 | } | 501 | } | ||
| 490 | } | 502 | } | ||
| 503 | + | ||||
| 504 | + | // estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation. | |||
| 505 | + | // This is a conservative estimate used only for early error detection. | |||
| 506 | + | func estimateStackBytes(ty reflect.Type) int { | |||
| 507 | + | numInts, numFloats := 0, 0 | |||
| 508 | + | stackBytes := 0 | |||
| 509 | + | ||||
| 510 | + | for i := 0; i < ty.NumIn(); i++ { | |||
| 511 | + | arg := ty.In(i) | |||
| 512 | + | size := int(arg.Size()) | |||
| 513 | + | ||||
| 514 | + | // Check if this goes to register or stack | |||
| 515 | + | usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64 | |||
| 516 | + | if usesInt && numInts < numOfIntegerRegisters() { | |||
| 517 | + | numInts++ | |||
| 518 | + | } else if !usesInt && numFloats < numOfFloatRegisters { | |||
| 519 | + | numFloats++ | |||
| 520 | + | } else { | |||
| 521 | + | // Goes to stack - accumulate total bytes | |||
| 522 | + | stackBytes += size | |||
| 523 | + | } | |||
| 524 | + | } | |||
| 525 | + | // Round total to 8-byte boundary | |||
| 526 | + | if stackBytes > 0 && stackBytes%align8ByteSize != 0 { | |||
| 527 | + | stackBytes = int(roundUpTo8(uintptr(stackBytes))) | |||
| 528 | + | } | |||
| 529 | + | return stackBytes | |||
| 530 | + | } | |||
| 531 | + | ||||
| 532 | + | // isCallbackFunction checks if the given function pointer is a purego callback. | |||
| 533 | + | // We need to detect this to avoid using tight packing for callbacks, since callback | |||
| 534 | + | // unpacking still uses the 8-byte slot convention. | |||
| 535 | + | // TODO: This function can be removed once Darwin ARM64 callbacks support tight packing. | |||
| 536 | + | // Once callbackWrap is updated to unpack C-style arguments, callbacks can use the same | |||
| 537 | + | // tight packing as normal C function calls. | |||
| 538 | + | func isCallbackFunction(cfn uintptr) bool { | |||
| 539 | + | // Only platforms with syscall_sysv.go have callback detection. | |||
| 540 | + | // Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd | |||
| 541 | + | hasSyscallSysv := runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" || | |||
| 542 | + | (runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64")) | |||
| 543 | + | if !hasSyscallSysv { | |||
| 544 | + | return false | |||
| 545 | + | } | |||
| 546 | + | ||||
| 547 | + | // Determine callback entry size based on architecture | |||
| 548 | + | var entrySize int | |||
| 549 | + | switch runtime.GOARCH { | |||
| 550 | + | case "386", "amd64": | |||
| 551 | + | entrySize = 5 | |||
| 552 | + | case "arm", "arm64", "loong64": | |||
| 553 | + | entrySize = 8 | |||
| 554 | + | default: | |||
| 555 | + | return false | |||
| 556 | + | } | |||
| 557 | + | ||||
| 558 | + | // Check if cfn is in the callback address range | |||
| 559 | + | callbackStart := getCallbackStart() | |||
| 560 | + | callbackEnd := callbackStart + uintptr(getMaxCB()*entrySize) | |||
| 561 | + | return cfn >= callbackStart && cfn < callbackEnd | |||
| 562 | + | } | |||
M func_test.go +229 -1
| @@ -4,6 +4,7 @@ | |||
| 4 | 4 | package purego_test | |
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | + | "bytes" | |
| 7 | 8 | "errors" | |
| 8 | 9 | "fmt" | |
| 9 | 10 | "os/exec" | |
| @@ -191,7 +192,7 @@ func TestABI(t *testing.T) { | |||
| 191 | 192 | purego.RegisterLibFunc(&fn, lib, cName) | |
| 192 | 193 | buf := make([]byte, 256) | |
| 193 | 194 | fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz") | |
| 194 | - | res := string(buf[:strings.IndexByte(string(buf), 0)]) | |
| 195 | + | res := string(buf[:bytes.IndexByte(buf, 0)]) | |
| 195 | 196 | const want = "1:2:3:4:5:6:7:8:foo:bar:baz" | |
| 196 | 197 | if res != want { | |
| 197 | 198 | t.Fatalf("%s: got %q, want %q", cName, res, want) | |
| @@ -199,6 +200,233 @@ func TestABI(t *testing.T) { | |||
| 199 | 200 | } | |
| 200 | 201 | } | |
| 201 | 202 | ||
| 203 | + | func TestABI_ArgumentPassing(t *testing.T) { | |
| 204 | + | if runtime.GOOS == "windows" && runtime.GOARCH == "386" { | |
| 205 | + | t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test | |
| 206 | + | } | |
| 207 | + | libFileName := filepath.Join(t.TempDir(), "abitest.so") | |
| 208 | + | if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil { | |
| 209 | + | t.Fatal(err) | |
| 210 | + | } | |
| 211 | + | lib, err := load.OpenLibrary(libFileName) | |
| 212 | + | if err != nil { | |
| 213 | + | t.Fatalf("Failed to open library %q: %v", libFileName, err) | |
| 214 | + | } | |
| 215 | + | t.Cleanup(func() { | |
| 216 | + | if err := load.CloseLibrary(lib); err != nil { | |
| 217 | + | t.Errorf("Failed to close library: %v", err) | |
| 218 | + | } | |
| 219 | + | }) | |
| 220 | + | ||
| 221 | + | tests := []struct { | |
| 222 | + | name string | |
| 223 | + | fn any | |
| 224 | + | cFn string | |
| 225 | + | call func(any) string | |
| 226 | + | want string | |
| 227 | + | }{ | |
| 228 | + | { | |
| 229 | + | name: "10_int32_baseline", | |
| 230 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)), | |
| 231 | + | cFn: "stack_10_int32", | |
| 232 | + | call: func(f any) string { | |
| 233 | + | buf := make([]byte, 256) | |
| 234 | + | (*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) | |
| 235 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 236 | + | }, | |
| 237 | + | want: "1:2:3:4:5:6:7:8:9:10", | |
| 238 | + | }, | |
| 239 | + | { | |
| 240 | + | name: "11_int32", | |
| 241 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)), | |
| 242 | + | cFn: "stack_11_int32", | |
| 243 | + | call: func(f any) string { | |
| 244 | + | buf := make([]byte, 256) | |
| 245 | + | (*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) | |
| 246 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 247 | + | }, | |
| 248 | + | want: "1:2:3:4:5:6:7:8:9:10:11", | |
| 249 | + | }, | |
| 250 | + | { | |
| 251 | + | name: "10_float32", | |
| 252 | + | fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)), | |
| 253 | + | cFn: "stack_10_float32", | |
| 254 | + | call: func(f any) string { | |
| 255 | + | buf := make([]byte, 256) | |
| 256 | + | (*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) | |
| 257 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 258 | + | }, | |
| 259 | + | want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0", | |
| 260 | + | }, | |
| 261 | + | { | |
| 262 | + | name: "mixed_stack_strings", | |
| 263 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)), | |
| 264 | + | cFn: "stack_mixed_stack_4args", | |
| 265 | + | call: func(f any) string { | |
| 266 | + | buf := make([]byte, 256) | |
| 267 | + | (*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") | |
| 268 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 269 | + | }, | |
| 270 | + | want: "1:2:3:4:5:6:7:8:foo:0:99:bar", | |
| 271 | + | }, | |
| 272 | + | { | |
| 273 | + | name: "20_int32", | |
| 274 | + | 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)), | |
| 275 | + | cFn: "stack_20_int32", | |
| 276 | + | call: func(f any) string { | |
| 277 | + | buf := make([]byte, 512) | |
| 278 | + | (*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) | |
| 279 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 280 | + | }, | |
| 281 | + | want: "1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20", | |
| 282 | + | }, | |
| 283 | + | { | |
| 284 | + | name: "8int_hfa2_stack", | |
| 285 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })), | |
| 286 | + | cFn: "stack_8int_hfa2_stack", | |
| 287 | + | call: func(f any) string { | |
| 288 | + | buf := make([]byte, 256) | |
| 289 | + | (*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}) | |
| 290 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 291 | + | }, | |
| 292 | + | want: "1:2:3:4:5:6:7:8:10.0:20.0", | |
| 293 | + | }, | |
| 294 | + | { | |
| 295 | + | name: "8int_2structs_stack", | |
| 296 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })), | |
| 297 | + | cFn: "stack_8int_2structs_stack", | |
| 298 | + | call: func(f any) string { | |
| 299 | + | buf := make([]byte, 256) | |
| 300 | + | (*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}) | |
| 301 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 302 | + | }, | |
| 303 | + | want: "1:2:3:4:5:6:7:8:9:10:11:12", | |
| 304 | + | }, | |
| 305 | + | { | |
| 306 | + | name: "8float_hfa2_stack", | |
| 307 | + | fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })), | |
| 308 | + | cFn: "stack_8float_hfa2_stack", | |
| 309 | + | call: func(f any) string { | |
| 310 | + | buf := make([]byte, 256) | |
| 311 | + | (*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}) | |
| 312 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 313 | + | }, | |
| 314 | + | want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0", | |
| 315 | + | }, | |
| 316 | + | { | |
| 317 | + | name: "8int_hfa2_floatregs", | |
| 318 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })), | |
| 319 | + | cFn: "stack_8int_hfa2_floatregs", | |
| 320 | + | call: func(f any) string { | |
| 321 | + | buf := make([]byte, 256) | |
| 322 | + | (*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}) | |
| 323 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 324 | + | }, | |
| 325 | + | want: "1:2:3:4:5:6:7:8:10.0:20.0", | |
| 326 | + | }, | |
| 327 | + | { | |
| 328 | + | name: "8int_int_struct_int", | |
| 329 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)), | |
| 330 | + | cFn: "stack_8int_int_struct_int", | |
| 331 | + | call: func(f any) string { | |
| 332 | + | buf := make([]byte, 256) | |
| 333 | + | (*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) | |
| 334 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 335 | + | }, | |
| 336 | + | want: "1:2:3:4:5:6:7:8:9:10:11:12", | |
| 337 | + | }, | |
| 338 | + | { | |
| 339 | + | name: "8int_hfa4_stack", | |
| 340 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })), | |
| 341 | + | cFn: "stack_8int_hfa4_stack", | |
| 342 | + | call: func(f any) string { | |
| 343 | + | buf := make([]byte, 256) | |
| 344 | + | (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y, z, w float32 }{10.0, 20.0, 30.0, 40.0}) | |
| 345 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 346 | + | }, | |
| 347 | + | want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0", | |
| 348 | + | }, | |
| 349 | + | { | |
| 350 | + | name: "8int_mixed_struct", | |
| 351 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct { | |
| 352 | + | a int32 | |
| 353 | + | b float32 | |
| 354 | + | })), | |
| 355 | + | cFn: "stack_8int_mixed_struct", | |
| 356 | + | call: func(f any) string { | |
| 357 | + | buf := make([]byte, 256) | |
| 358 | + | (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct { | |
| 359 | + | a int32 | |
| 360 | + | b float32 | |
| 361 | + | })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct { | |
| 362 | + | a int32 | |
| 363 | + | b float32 | |
| 364 | + | }{9, 10.0}) | |
| 365 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |
| 366 | + | }, | |
| 367 | + | want: "1:2:3:4:5:6:7:8:9:10.0", | |
| 368 | + | }, | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | for _, tt := range tests { | |
| 372 | + | t.Run(tt.name, func(t *testing.T) { | |
| 373 | + | if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") { | |
| 374 | + | t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking") | |
| 375 | + | } | |
| 376 | + | if tt.name == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64") { | |
| 377 | + | t.Skip("float32 stack arguments not yet supported on this platform") | |
| 378 | + | } | |
| 379 | + | // Struct tests require Darwin ARM64 or AMD64 | |
| 380 | + | if strings.HasPrefix(tt.name, "8int_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) { | |
| 381 | + | t.Skip("struct argument tests only supported on Darwin ARM64/AMD64") | |
| 382 | + | } | |
| 383 | + | if strings.HasPrefix(tt.name, "8float_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) { | |
| 384 | + | t.Skip("struct argument tests only supported on Darwin ARM64/AMD64") | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | purego.RegisterLibFunc(tt.fn, lib, tt.cFn) | |
| 388 | + | got := tt.call(tt.fn) | |
| 389 | + | if got != tt.want { | |
| 390 | + | t.Errorf("%s\n got: %q\n want: %q", tt.cFn, got, tt.want) | |
| 391 | + | } | |
| 392 | + | }) | |
| 393 | + | } | |
| 394 | + | } | |
| 395 | + | ||
| 396 | + | func TestABI_TooManyArguments(t *testing.T) { | |
| 397 | + | if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" { | |
| 398 | + | t.Skip("This test is specific to Darwin ARM64") | |
| 399 | + | } | |
| 400 | + | ||
| 401 | + | libFileName := filepath.Join(t.TempDir(), "abitest.so") | |
| 402 | + | if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil { | |
| 403 | + | t.Fatal(err) | |
| 404 | + | } | |
| 405 | + | lib, err := load.OpenLibrary(libFileName) | |
| 406 | + | if err != nil { | |
| 407 | + | t.Fatalf("Failed to open library %q: %v", libFileName, err) | |
| 408 | + | } | |
| 409 | + | t.Cleanup(func() { | |
| 410 | + | if err := load.CloseLibrary(lib); err != nil { | |
| 411 | + | t.Errorf("Failed to close library: %v", err) | |
| 412 | + | } | |
| 413 | + | }) | |
| 414 | + | ||
| 415 | + | // Test that 25 int64 arguments (17 slots needed) exceeds the limit | |
| 416 | + | t.Run("25_int64_exceeds_limit", func(t *testing.T) { | |
| 417 | + | defer func() { | |
| 418 | + | if r := recover(); r != nil { | |
| 419 | + | t.Logf("Got expected panic: %v", r) | |
| 420 | + | } else { | |
| 421 | + | t.Errorf("Expected panic but didn't get one") | |
| 422 | + | } | |
| 423 | + | }() | |
| 424 | + | ||
| 425 | + | var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) | |
| 426 | + | purego.RegisterLibFunc(&fn, lib, "stack_25_int64_exceeds") | |
| 427 | + | }) | |
| 428 | + | } | |
| 429 | + | ||
| 202 | 430 | func buildSharedLib(compilerEnv, libFile string, sources ...string) error { | |
| 203 | 431 | out, err := exec.Command("go", "env", compilerEnv).Output() | |
| 204 | 432 | if err != nil { | |
| @@ -4,6 +4,7 @@ | |||||
| 4 | package purego_test | 4 | package purego_test | ||
| 5 | 5 | ||||
| 6 | import ( | 6 | import ( | ||
| 7 | + | "bytes" | |||
| 7 | "errors" | 8 | "errors" | ||
| 8 | "fmt" | 9 | "fmt" | ||
| 9 | "os/exec" | 10 | "os/exec" | ||
| @@ -191,7 +192,7 @@ func TestABI(t *testing.T) { | |||||
| 191 | purego.RegisterLibFunc(&fn, lib, cName) | 192 | purego.RegisterLibFunc(&fn, lib, cName) | ||
| 192 | buf := make([]byte, 256) | 193 | buf := make([]byte, 256) | ||
| 193 | fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz") | 194 | fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz") | ||
| 194 | - | res := string(buf[:strings.IndexByte(string(buf), 0)]) | 195 | + | res := string(buf[:bytes.IndexByte(buf, 0)]) |
| 195 | const want = "1:2:3:4:5:6:7:8:foo:bar:baz" | 196 | const want = "1:2:3:4:5:6:7:8:foo:bar:baz" | ||
| 196 | if res != want { | 197 | if res != want { | ||
| 197 | t.Fatalf("%s: got %q, want %q", cName, res, want) | 198 | t.Fatalf("%s: got %q, want %q", cName, res, want) | ||
| @@ -199,6 +200,233 @@ func TestABI(t *testing.T) { | |||||
| 199 | } | 200 | } | ||
| 200 | } | 201 | } | ||
| 201 | 202 | ||||
| 203 | + | func TestABI_ArgumentPassing(t *testing.T) { | |||
| 204 | + | if runtime.GOOS == "windows" && runtime.GOARCH == "386" { | |||
| 205 | + | t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test | |||
| 206 | + | } | |||
| 207 | + | libFileName := filepath.Join(t.TempDir(), "abitest.so") | |||
| 208 | + | if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil { | |||
| 209 | + | t.Fatal(err) | |||
| 210 | + | } | |||
| 211 | + | lib, err := load.OpenLibrary(libFileName) | |||
| 212 | + | if err != nil { | |||
| 213 | + | t.Fatalf("Failed to open library %q: %v", libFileName, err) | |||
| 214 | + | } | |||
| 215 | + | t.Cleanup(func() { | |||
| 216 | + | if err := load.CloseLibrary(lib); err != nil { | |||
| 217 | + | t.Errorf("Failed to close library: %v", err) | |||
| 218 | + | } | |||
| 219 | + | }) | |||
| 220 | + | ||||
| 221 | + | tests := []struct { | |||
| 222 | + | name string | |||
| 223 | + | fn any | |||
| 224 | + | cFn string | |||
| 225 | + | call func(any) string | |||
| 226 | + | want string | |||
| 227 | + | }{ | |||
| 228 | + | { | |||
| 229 | + | name: "10_int32_baseline", | |||
| 230 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)), | |||
| 231 | + | cFn: "stack_10_int32", | |||
| 232 | + | call: func(f any) string { | |||
| 233 | + | buf := make([]byte, 256) | |||
| 234 | + | (*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) | |||
| 235 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 236 | + | }, | |||
| 237 | + | want: "1:2:3:4:5:6:7:8:9:10", | |||
| 238 | + | }, | |||
| 239 | + | { | |||
| 240 | + | name: "11_int32", | |||
| 241 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)), | |||
| 242 | + | cFn: "stack_11_int32", | |||
| 243 | + | call: func(f any) string { | |||
| 244 | + | buf := make([]byte, 256) | |||
| 245 | + | (*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) | |||
| 246 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 247 | + | }, | |||
| 248 | + | want: "1:2:3:4:5:6:7:8:9:10:11", | |||
| 249 | + | }, | |||
| 250 | + | { | |||
| 251 | + | name: "10_float32", | |||
| 252 | + | fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)), | |||
| 253 | + | cFn: "stack_10_float32", | |||
| 254 | + | call: func(f any) string { | |||
| 255 | + | buf := make([]byte, 256) | |||
| 256 | + | (*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) | |||
| 257 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 258 | + | }, | |||
| 259 | + | want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0", | |||
| 260 | + | }, | |||
| 261 | + | { | |||
| 262 | + | name: "mixed_stack_strings", | |||
| 263 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)), | |||
| 264 | + | cFn: "stack_mixed_stack_4args", | |||
| 265 | + | call: func(f any) string { | |||
| 266 | + | buf := make([]byte, 256) | |||
| 267 | + | (*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") | |||
| 268 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 269 | + | }, | |||
| 270 | + | want: "1:2:3:4:5:6:7:8:foo:0:99:bar", | |||
| 271 | + | }, | |||
| 272 | + | { | |||
| 273 | + | name: "20_int32", | |||
| 274 | + | 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)), | |||
| 275 | + | cFn: "stack_20_int32", | |||
| 276 | + | call: func(f any) string { | |||
| 277 | + | buf := make([]byte, 512) | |||
| 278 | + | (*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) | |||
| 279 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 280 | + | }, | |||
| 281 | + | want: "1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20", | |||
| 282 | + | }, | |||
| 283 | + | { | |||
| 284 | + | name: "8int_hfa2_stack", | |||
| 285 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })), | |||
| 286 | + | cFn: "stack_8int_hfa2_stack", | |||
| 287 | + | call: func(f any) string { | |||
| 288 | + | buf := make([]byte, 256) | |||
| 289 | + | (*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}) | |||
| 290 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 291 | + | }, | |||
| 292 | + | want: "1:2:3:4:5:6:7:8:10.0:20.0", | |||
| 293 | + | }, | |||
| 294 | + | { | |||
| 295 | + | name: "8int_2structs_stack", | |||
| 296 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })), | |||
| 297 | + | cFn: "stack_8int_2structs_stack", | |||
| 298 | + | call: func(f any) string { | |||
| 299 | + | buf := make([]byte, 256) | |||
| 300 | + | (*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}) | |||
| 301 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 302 | + | }, | |||
| 303 | + | want: "1:2:3:4:5:6:7:8:9:10:11:12", | |||
| 304 | + | }, | |||
| 305 | + | { | |||
| 306 | + | name: "8float_hfa2_stack", | |||
| 307 | + | fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })), | |||
| 308 | + | cFn: "stack_8float_hfa2_stack", | |||
| 309 | + | call: func(f any) string { | |||
| 310 | + | buf := make([]byte, 256) | |||
| 311 | + | (*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}) | |||
| 312 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 313 | + | }, | |||
| 314 | + | want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0", | |||
| 315 | + | }, | |||
| 316 | + | { | |||
| 317 | + | name: "8int_hfa2_floatregs", | |||
| 318 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })), | |||
| 319 | + | cFn: "stack_8int_hfa2_floatregs", | |||
| 320 | + | call: func(f any) string { | |||
| 321 | + | buf := make([]byte, 256) | |||
| 322 | + | (*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}) | |||
| 323 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 324 | + | }, | |||
| 325 | + | want: "1:2:3:4:5:6:7:8:10.0:20.0", | |||
| 326 | + | }, | |||
| 327 | + | { | |||
| 328 | + | name: "8int_int_struct_int", | |||
| 329 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)), | |||
| 330 | + | cFn: "stack_8int_int_struct_int", | |||
| 331 | + | call: func(f any) string { | |||
| 332 | + | buf := make([]byte, 256) | |||
| 333 | + | (*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) | |||
| 334 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 335 | + | }, | |||
| 336 | + | want: "1:2:3:4:5:6:7:8:9:10:11:12", | |||
| 337 | + | }, | |||
| 338 | + | { | |||
| 339 | + | name: "8int_hfa4_stack", | |||
| 340 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })), | |||
| 341 | + | cFn: "stack_8int_hfa4_stack", | |||
| 342 | + | call: func(f any) string { | |||
| 343 | + | buf := make([]byte, 256) | |||
| 344 | + | (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y, z, w float32 }{10.0, 20.0, 30.0, 40.0}) | |||
| 345 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 346 | + | }, | |||
| 347 | + | want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0", | |||
| 348 | + | }, | |||
| 349 | + | { | |||
| 350 | + | name: "8int_mixed_struct", | |||
| 351 | + | fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct { | |||
| 352 | + | a int32 | |||
| 353 | + | b float32 | |||
| 354 | + | })), | |||
| 355 | + | cFn: "stack_8int_mixed_struct", | |||
| 356 | + | call: func(f any) string { | |||
| 357 | + | buf := make([]byte, 256) | |||
| 358 | + | (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct { | |||
| 359 | + | a int32 | |||
| 360 | + | b float32 | |||
| 361 | + | })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct { | |||
| 362 | + | a int32 | |||
| 363 | + | b float32 | |||
| 364 | + | }{9, 10.0}) | |||
| 365 | + | return string(buf[:bytes.IndexByte(buf, 0)]) | |||
| 366 | + | }, | |||
| 367 | + | want: "1:2:3:4:5:6:7:8:9:10.0", | |||
| 368 | + | }, | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | for _, tt := range tests { | |||
| 372 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 373 | + | if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") { | |||
| 374 | + | t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking") | |||
| 375 | + | } | |||
| 376 | + | if tt.name == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64") { | |||
| 377 | + | t.Skip("float32 stack arguments not yet supported on this platform") | |||
| 378 | + | } | |||
| 379 | + | // Struct tests require Darwin ARM64 or AMD64 | |||
| 380 | + | if strings.HasPrefix(tt.name, "8int_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) { | |||
| 381 | + | t.Skip("struct argument tests only supported on Darwin ARM64/AMD64") | |||
| 382 | + | } | |||
| 383 | + | if strings.HasPrefix(tt.name, "8float_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) { | |||
| 384 | + | t.Skip("struct argument tests only supported on Darwin ARM64/AMD64") | |||
| 385 | + | } | |||
| 386 | + | ||||
| 387 | + | purego.RegisterLibFunc(tt.fn, lib, tt.cFn) | |||
| 388 | + | got := tt.call(tt.fn) | |||
| 389 | + | if got != tt.want { | |||
| 390 | + | t.Errorf("%s\n got: %q\n want: %q", tt.cFn, got, tt.want) | |||
| 391 | + | } | |||
| 392 | + | }) | |||
| 393 | + | } | |||
| 394 | + | } | |||
| 395 | + | ||||
| 396 | + | func TestABI_TooManyArguments(t *testing.T) { | |||
| 397 | + | if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" { | |||
| 398 | + | t.Skip("This test is specific to Darwin ARM64") | |||
| 399 | + | } | |||
| 400 | + | ||||
| 401 | + | libFileName := filepath.Join(t.TempDir(), "abitest.so") | |||
| 402 | + | if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil { | |||
| 403 | + | t.Fatal(err) | |||
| 404 | + | } | |||
| 405 | + | lib, err := load.OpenLibrary(libFileName) | |||
| 406 | + | if err != nil { | |||
| 407 | + | t.Fatalf("Failed to open library %q: %v", libFileName, err) | |||
| 408 | + | } | |||
| 409 | + | t.Cleanup(func() { | |||
| 410 | + | if err := load.CloseLibrary(lib); err != nil { | |||
| 411 | + | t.Errorf("Failed to close library: %v", err) | |||
| 412 | + | } | |||
| 413 | + | }) | |||
| 414 | + | ||||
| 415 | + | // Test that 25 int64 arguments (17 slots needed) exceeds the limit | |||
| 416 | + | t.Run("25_int64_exceeds_limit", func(t *testing.T) { | |||
| 417 | + | defer func() { | |||
| 418 | + | if r := recover(); r != nil { | |||
| 419 | + | t.Logf("Got expected panic: %v", r) | |||
| 420 | + | } else { | |||
| 421 | + | t.Errorf("Expected panic but didn't get one") | |||
| 422 | + | } | |||
| 423 | + | }() | |||
| 424 | + | ||||
| 425 | + | var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) | |||
| 426 | + | purego.RegisterLibFunc(&fn, lib, "stack_25_int64_exceeds") | |||
| 427 | + | }) | |||
| 428 | + | } | |||
| 429 | + | ||||
| 202 | func buildSharedLib(compilerEnv, libFile string, sources ...string) error { | 430 | func buildSharedLib(compilerEnv, libFile string, sources ...string) error { | ||
| 203 | out, err := exec.Command("go", "env", compilerEnv).Output() | 431 | out, err := exec.Command("go", "env", compilerEnv).Output() | ||
| 204 | if err != nil { | 432 | if err != nil { | ||
A struct_386.go +41 -0
| @@ -0,0 +1,41 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | package purego | |
| 5 | + | ||
| 6 | + | import "reflect" | |
| 7 | + | ||
| 8 | + | func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { | |
| 9 | + | panic("purego: struct arguments are not supported") | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | |
| 13 | + | panic("purego: struct returns are not supported") | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |
| 17 | + | panic("purego: placeRegisters not implemented on 386") | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // shouldBundleStackArgs always returns false on 386 | |
| 21 | + | // since C-style stack argument bundling is only needed on Darwin ARM64. | |
| 22 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |
| 23 | + | return false | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // structFitsInRegisters is not used on 386. | |
| 27 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |
| 28 | + | panic("purego: structFitsInRegisters should not be called on 386") | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // collectStackArgs is not used on 386. | |
| 32 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |
| 33 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |
| 34 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |
| 35 | + | panic("purego: collectStackArgs should not be called on 386") | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // bundleStackArgs is not used on 386. | |
| 39 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |
| 40 | + | panic("purego: bundleStackArgs should not be called on 386") | |
| 41 | + | } | |
| @@ -0,0 +1,41 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | package purego | |||
| 5 | + | ||||
| 6 | + | import "reflect" | |||
| 7 | + | ||||
| 8 | + | func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { | |||
| 9 | + | panic("purego: struct arguments are not supported") | |||
| 10 | + | } | |||
| 11 | + | ||||
| 12 | + | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | |||
| 13 | + | panic("purego: struct returns are not supported") | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |||
| 17 | + | panic("purego: placeRegisters not implemented on 386") | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // shouldBundleStackArgs always returns false on 386 | |||
| 21 | + | // since C-style stack argument bundling is only needed on Darwin ARM64. | |||
| 22 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |||
| 23 | + | return false | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // structFitsInRegisters is not used on 386. | |||
| 27 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |||
| 28 | + | panic("purego: structFitsInRegisters should not be called on 386") | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // collectStackArgs is not used on 386. | |||
| 32 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |||
| 33 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |||
| 34 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |||
| 35 | + | panic("purego: collectStackArgs should not be called on 386") | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // bundleStackArgs is not used on 386. | |||
| 39 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |||
| 40 | + | panic("purego: bundleStackArgs should not be called on 386") | |||
| 41 | + | } | |||
M struct_amd64.go +24 -1
| @@ -260,5 +260,28 @@ func placeStack(v reflect.Value, addStack func(uintptr)) { | |||
| 260 | 260 | } | |
| 261 | 261 | ||
| 262 | 262 | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |
| 263 | - | panic("purego: not needed on amd64") | |
| 263 | + | panic("purego: placeRegisters not implemented on amd64") | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | // shouldBundleStackArgs always returns false on non-Darwin platforms | |
| 267 | + | // since C-style stack argument bundling is only needed on Darwin ARM64. | |
| 268 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |
| 269 | + | return false | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | // structFitsInRegisters is not used on amd64. | |
| 273 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |
| 274 | + | panic("purego: structFitsInRegisters should not be called on amd64") | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | // collectStackArgs is not used on amd64. | |
| 278 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |
| 279 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |
| 280 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |
| 281 | + | panic("purego: collectStackArgs should not be called on amd64") | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | // bundleStackArgs is not used on amd64. | |
| 285 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |
| 286 | + | panic("purego: bundleStackArgs should not be called on amd64") | |
| 264 | 287 | } | |
| @@ -260,5 +260,28 @@ func placeStack(v reflect.Value, addStack func(uintptr)) { | |||||
| 260 | } | 260 | } | ||
| 261 | 261 | ||||
| 262 | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | 262 | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | ||
| 263 | - | panic("purego: not needed on amd64") | 263 | + | panic("purego: placeRegisters not implemented on amd64") |
| 264 | + | } | |||
| 265 | + | ||||
| 266 | + | // shouldBundleStackArgs always returns false on non-Darwin platforms | |||
| 267 | + | // since C-style stack argument bundling is only needed on Darwin ARM64. | |||
| 268 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |||
| 269 | + | return false | |||
| 270 | + | } | |||
| 271 | + | ||||
| 272 | + | // structFitsInRegisters is not used on amd64. | |||
| 273 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |||
| 274 | + | panic("purego: structFitsInRegisters should not be called on amd64") | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | // collectStackArgs is not used on amd64. | |||
| 278 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |||
| 279 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |||
| 280 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |||
| 281 | + | panic("purego: collectStackArgs should not be called on amd64") | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | // bundleStackArgs is not used on amd64. | |||
| 285 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |||
| 286 | + | panic("purego: bundleStackArgs should not be called on amd64") | |||
| 264 | } | 287 | } | ||
A struct_arm.go +41 -0
| @@ -0,0 +1,41 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | package purego | |
| 5 | + | ||
| 6 | + | import "reflect" | |
| 7 | + | ||
| 8 | + | func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { | |
| 9 | + | panic("purego: struct arguments are not supported") | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | |
| 13 | + | panic("purego: struct returns are not supported") | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |
| 17 | + | panic("purego: placeRegisters not implemented on arm") | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // shouldBundleStackArgs always returns false on arm | |
| 21 | + | // since C-style stack argument bundling is only needed on Darwin ARM64. | |
| 22 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |
| 23 | + | return false | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // structFitsInRegisters is not used on arm. | |
| 27 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |
| 28 | + | panic("purego: structFitsInRegisters should not be called on arm") | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // collectStackArgs is not used on arm. | |
| 32 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |
| 33 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |
| 34 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |
| 35 | + | panic("purego: collectStackArgs should not be called on arm") | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // bundleStackArgs is not used on arm. | |
| 39 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |
| 40 | + | panic("purego: bundleStackArgs should not be called on arm") | |
| 41 | + | } | |
| @@ -0,0 +1,41 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | package purego | |||
| 5 | + | ||||
| 6 | + | import "reflect" | |||
| 7 | + | ||||
| 8 | + | func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { | |||
| 9 | + | panic("purego: struct arguments are not supported") | |||
| 10 | + | } | |||
| 11 | + | ||||
| 12 | + | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | |||
| 13 | + | panic("purego: struct returns are not supported") | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |||
| 17 | + | panic("purego: placeRegisters not implemented on arm") | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // shouldBundleStackArgs always returns false on arm | |||
| 21 | + | // since C-style stack argument bundling is only needed on Darwin ARM64. | |||
| 22 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |||
| 23 | + | return false | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // structFitsInRegisters is not used on arm. | |||
| 27 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |||
| 28 | + | panic("purego: structFitsInRegisters should not be called on arm") | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // collectStackArgs is not used on arm. | |||
| 32 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |||
| 33 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |||
| 34 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |||
| 35 | + | panic("purego: collectStackArgs should not be called on arm") | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // bundleStackArgs is not used on arm. | |||
| 39 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |||
| 40 | + | panic("purego: bundleStackArgs should not be called on arm") | |||
| 41 | + | } | |||
M struct_arm64.go +263 -0
| @@ -6,7 +6,12 @@ package purego | |||
| 6 | 6 | import ( | |
| 7 | 7 | "math" | |
| 8 | 8 | "reflect" | |
| 9 | + | "runtime" | |
| 10 | + | "strconv" | |
| 11 | + | stdstrings "strings" | |
| 9 | 12 | "unsafe" | |
| 13 | + | ||
| 14 | + | "github.com/ebitengine/purego/internal/strings" | |
| 10 | 15 | ) | |
| 11 | 16 | ||
| 12 | 17 | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | |
| @@ -87,6 +92,14 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl | |||
| 87 | 92 | } | |
| 88 | 93 | ||
| 89 | 94 | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |
| 95 | + | if runtime.GOOS == "darwin" { | |
| 96 | + | placeRegistersDarwin(v, addFloat, addInt) | |
| 97 | + | return | |
| 98 | + | } | |
| 99 | + | placeRegistersArm64(v, addFloat, addInt) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func placeRegistersArm64(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |
| 90 | 103 | var val uint64 | |
| 91 | 104 | var shift byte | |
| 92 | 105 | var flushed bool | |
| @@ -284,3 +297,253 @@ func isHVA(t reflect.Type) bool { | |||
| 284 | 297 | return false | |
| 285 | 298 | } | |
| 286 | 299 | } | |
| 300 | + | ||
| 301 | + | // copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback. | |
| 302 | + | // This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs. | |
| 303 | + | func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) { | |
| 304 | + | if runtime.GOOS != "darwin" { | |
| 305 | + | panic("purego: should only be called on darwin") | |
| 306 | + | } | |
| 307 | + | for offset := uintptr(0); offset < size; offset += 8 { | |
| 308 | + | var chunk uintptr | |
| 309 | + | remaining := size - offset | |
| 310 | + | if remaining >= 8 { | |
| 311 | + | chunk = *(*uintptr)(unsafe.Add(ptr, offset)) | |
| 312 | + | } else { | |
| 313 | + | // Read byte-by-byte to avoid reading beyond allocation | |
| 314 | + | for i := uintptr(0); i < remaining; i++ { | |
| 315 | + | b := *(*byte)(unsafe.Add(ptr, offset+i)) | |
| 316 | + | chunk |= uintptr(b) << (i * 8) | |
| 317 | + | } | |
| 318 | + | } | |
| 319 | + | addChunk(chunk) | |
| 320 | + | } | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | // placeRegisters implements Darwin ARM64 calling convention for struct arguments. | |
| 324 | + | // | |
| 325 | + | // For HFA/HVA structs, each element must go in a separate register (or stack slot for elements | |
| 326 | + | // that don't fit in registers). We use placeRegistersArm64 for this. | |
| 327 | + | // | |
| 328 | + | // For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in | |
| 329 | + | // 8-byte chunks, which works correctly for both register and stack placement. | |
| 330 | + | func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |
| 331 | + | if runtime.GOOS != "darwin" { | |
| 332 | + | panic("purego: placeRegistersDarwin should only be called on darwin") | |
| 333 | + | } | |
| 334 | + | // Check if this is an HFA/HVA | |
| 335 | + | hfa := isHFA(v.Type()) | |
| 336 | + | hva := isHVA(v.Type()) | |
| 337 | + | ||
| 338 | + | // For HFA/HVA structs, use the standard ARM64 logic which places each element separately | |
| 339 | + | if hfa || hva { | |
| 340 | + | placeRegistersArm64(v, addFloat, addInt) | |
| 341 | + | return | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | // For non-HFA/HVA structs, use byte-level copying | |
| 345 | + | // If the value is not addressable, create an addressable copy | |
| 346 | + | if !v.CanAddr() { | |
| 347 | + | addressable := reflect.New(v.Type()).Elem() | |
| 348 | + | addressable.Set(v) | |
| 349 | + | v = addressable | |
| 350 | + | } | |
| 351 | + | ptr := unsafe.Pointer(v.Addr().Pointer()) | |
| 352 | + | size := v.Type().Size() | |
| 353 | + | copyStruct8ByteChunks(ptr, size, addInt) | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | // shouldBundleStackArgs determines if we need to start C-style packing for | |
| 357 | + | // Darwin ARM64 stack arguments. This happens when registers are exhausted. | |
| 358 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |
| 359 | + | if runtime.GOOS != "darwin" { | |
| 360 | + | return false | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | kind := v.Kind() | |
| 364 | + | isFloat := kind == reflect.Float32 || kind == reflect.Float64 | |
| 365 | + | isInt := !isFloat && kind != reflect.Struct | |
| 366 | + | primitiveOnStack := | |
| 367 | + | (isInt && numInts >= numOfIntegerRegisters()) || | |
| 368 | + | (isFloat && numFloats >= numOfFloatRegisters) | |
| 369 | + | if primitiveOnStack { | |
| 370 | + | return true | |
| 371 | + | } | |
| 372 | + | if kind != reflect.Struct { | |
| 373 | + | return false | |
| 374 | + | } | |
| 375 | + | hfa := isHFA(v.Type()) | |
| 376 | + | hva := isHVA(v.Type()) | |
| 377 | + | size := v.Type().Size() | |
| 378 | + | eligible := hfa || hva || size <= 16 | |
| 379 | + | if !eligible { | |
| 380 | + | return false | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | if hfa { | |
| 384 | + | need := v.NumField() | |
| 385 | + | return numFloats+need > numOfFloatRegisters | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | if hva { | |
| 389 | + | need := v.NumField() | |
| 390 | + | return numInts+need > numOfIntegerRegisters() | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | slotsNeeded := int((size + align8ByteMask) / align8ByteSize) | |
| 394 | + | return numInts+slotsNeeded > numOfIntegerRegisters() | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | // structFitsInRegisters determines if a struct can still fit in remaining | |
| 398 | + | // registers, used during stack argument bundling to decide if a struct | |
| 399 | + | // should go through normal register allocation or be bundled with stack args. | |
| 400 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |
| 401 | + | if runtime.GOOS != "darwin" { | |
| 402 | + | panic("purego: structFitsInRegisters should only be called on darwin") | |
| 403 | + | } | |
| 404 | + | hfa := isHFA(val.Type()) | |
| 405 | + | hva := isHVA(val.Type()) | |
| 406 | + | size := val.Type().Size() | |
| 407 | + | ||
| 408 | + | if hfa { | |
| 409 | + | // HFA: check if elements fit in float registers | |
| 410 | + | if tempNumFloats+val.NumField() <= numOfFloatRegisters { | |
| 411 | + | return true, tempNumInts, tempNumFloats + val.NumField() | |
| 412 | + | } | |
| 413 | + | } else if hva { | |
| 414 | + | // HVA: check if elements fit in int registers | |
| 415 | + | if tempNumInts+val.NumField() <= numOfIntegerRegisters() { | |
| 416 | + | return true, tempNumInts + val.NumField(), tempNumFloats | |
| 417 | + | } | |
| 418 | + | } else if size <= 16 { | |
| 419 | + | // Non-HFA/HVA small structs use int registers for byte-packing | |
| 420 | + | slotsNeeded := int((size + align8ByteMask) / align8ByteSize) | |
| 421 | + | if tempNumInts+slotsNeeded <= numOfIntegerRegisters() { | |
| 422 | + | return true, tempNumInts + slotsNeeded, tempNumFloats | |
| 423 | + | } | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | return false, tempNumInts, tempNumFloats | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | // collectStackArgs separates remaining arguments into those that fit in registers vs those that go on stack. | |
| 430 | + | // It returns the stack arguments and processes register arguments through addValue. | |
| 431 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |
| 432 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |
| 433 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |
| 434 | + | if runtime.GOOS != "darwin" { | |
| 435 | + | panic("purego: collectStackArgs should only be called on darwin") | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | var stackArgs []reflect.Value | |
| 439 | + | tempNumInts := numInts | |
| 440 | + | tempNumFloats := numFloats | |
| 441 | + | ||
| 442 | + | for j, val := range args[startIdx:] { | |
| 443 | + | // Determine if this argument goes to register or stack | |
| 444 | + | var fitsInRegister bool | |
| 445 | + | var newNumInts, newNumFloats int | |
| 446 | + | ||
| 447 | + | if val.Kind() == reflect.Struct { | |
| 448 | + | // Check if struct still fits in remaining registers | |
| 449 | + | fitsInRegister, newNumInts, newNumFloats = structFitsInRegisters(val, tempNumInts, tempNumFloats) | |
| 450 | + | } else { | |
| 451 | + | // Primitive argument | |
| 452 | + | isFloat := val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64 | |
| 453 | + | if isFloat { | |
| 454 | + | fitsInRegister = tempNumFloats < numOfFloatRegisters | |
| 455 | + | newNumFloats = tempNumFloats + 1 | |
| 456 | + | newNumInts = tempNumInts | |
| 457 | + | } else { | |
| 458 | + | fitsInRegister = tempNumInts < numOfIntegerRegisters() | |
| 459 | + | newNumInts = tempNumInts + 1 | |
| 460 | + | newNumFloats = tempNumFloats | |
| 461 | + | } | |
| 462 | + | } | |
| 463 | + | ||
| 464 | + | if fitsInRegister { | |
| 465 | + | // Process through normal register allocation | |
| 466 | + | tempNumInts = newNumInts | |
| 467 | + | tempNumFloats = newNumFloats | |
| 468 | + | keepAlive = addValue(val, keepAlive, addInt, addFloat, addStack, pNumInts, pNumFloats, pNumStack) | |
| 469 | + | } else { | |
| 470 | + | // Convert strings to C strings before bundling | |
| 471 | + | if val.Kind() == reflect.String { | |
| 472 | + | ptr := strings.CString(val.String()) | |
| 473 | + | keepAlive = append(keepAlive, ptr) | |
| 474 | + | val = reflect.ValueOf(ptr) | |
| 475 | + | args[startIdx+j] = val | |
| 476 | + | } | |
| 477 | + | stackArgs = append(stackArgs, val) | |
| 478 | + | } | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | return stackArgs, keepAlive | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | const ( | |
| 485 | + | paddingFieldPrefix = "Pad" | |
| 486 | + | ) | |
| 487 | + | ||
| 488 | + | // bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing. | |
| 489 | + | // It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks. | |
| 490 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |
| 491 | + | if runtime.GOOS != "darwin" { | |
| 492 | + | panic("purego: bundleStackArgs should only be called on darwin") | |
| 493 | + | } | |
| 494 | + | if len(stackArgs) == 0 { | |
| 495 | + | return | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | // Build struct fields with proper C alignment and padding | |
| 499 | + | var fields []reflect.StructField | |
| 500 | + | currentOffset := uintptr(0) | |
| 501 | + | fieldIndex := 0 | |
| 502 | + | ||
| 503 | + | for j, val := range stackArgs { | |
| 504 | + | valSize := val.Type().Size() | |
| 505 | + | valAlign := val.Type().Align() | |
| 506 | + | ||
| 507 | + | // ARM64 requires 8-byte alignment for 8-byte or larger structs | |
| 508 | + | if val.Kind() == reflect.Struct && valSize >= 8 { | |
| 509 | + | valAlign = 8 | |
| 510 | + | } | |
| 511 | + | ||
| 512 | + | // Add padding field if needed for alignment | |
| 513 | + | if currentOffset%uintptr(valAlign) != 0 { | |
| 514 | + | paddingNeeded := uintptr(valAlign) - (currentOffset % uintptr(valAlign)) | |
| 515 | + | fields = append(fields, reflect.StructField{ | |
| 516 | + | Name: paddingFieldPrefix + strconv.Itoa(fieldIndex), | |
| 517 | + | Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeOf(byte(0))), | |
| 518 | + | }) | |
| 519 | + | currentOffset += paddingNeeded | |
| 520 | + | fieldIndex++ | |
| 521 | + | } | |
| 522 | + | ||
| 523 | + | fields = append(fields, reflect.StructField{ | |
| 524 | + | Name: "X" + strconv.Itoa(j), | |
| 525 | + | Type: val.Type(), | |
| 526 | + | }) | |
| 527 | + | currentOffset += valSize | |
| 528 | + | fieldIndex++ | |
| 529 | + | } | |
| 530 | + | ||
| 531 | + | // Create and populate the packed struct | |
| 532 | + | structType := reflect.StructOf(fields) | |
| 533 | + | structInstance := reflect.New(structType).Elem() | |
| 534 | + | ||
| 535 | + | // Set values (skip padding fields) | |
| 536 | + | argIndex := 0 | |
| 537 | + | for j := 0; j < structInstance.NumField(); j++ { | |
| 538 | + | fieldName := structType.Field(j).Name | |
| 539 | + | if stdstrings.HasPrefix(fieldName, paddingFieldPrefix) { | |
| 540 | + | continue | |
| 541 | + | } | |
| 542 | + | structInstance.Field(j).Set(stackArgs[argIndex]) | |
| 543 | + | argIndex++ | |
| 544 | + | } | |
| 545 | + | ||
| 546 | + | ptr := unsafe.Pointer(structInstance.Addr().Pointer()) | |
| 547 | + | size := structType.Size() | |
| 548 | + | copyStruct8ByteChunks(ptr, size, addStack) | |
| 549 | + | } | |
| @@ -6,7 +6,12 @@ package purego | |||||
| 6 | import ( | 6 | import ( | ||
| 7 | "math" | 7 | "math" | ||
| 8 | "reflect" | 8 | "reflect" | ||
| 9 | + | "runtime" | |||
| 10 | + | "strconv" | |||
| 11 | + | stdstrings "strings" | |||
| 9 | "unsafe" | 12 | "unsafe" | ||
| 13 | + | ||||
| 14 | + | "github.com/ebitengine/purego/internal/strings" | |||
| 10 | ) | 15 | ) | ||
| 11 | 16 | ||||
| 12 | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | 17 | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | ||
| @@ -87,6 +92,14 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl | |||||
| 87 | } | 92 | } | ||
| 88 | 93 | ||||
| 89 | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | 94 | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | ||
| 95 | + | if runtime.GOOS == "darwin" { | |||
| 96 | + | placeRegistersDarwin(v, addFloat, addInt) | |||
| 97 | + | return | |||
| 98 | + | } | |||
| 99 | + | placeRegistersArm64(v, addFloat, addInt) | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func placeRegistersArm64(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |||
| 90 | var val uint64 | 103 | var val uint64 | ||
| 91 | var shift byte | 104 | var shift byte | ||
| 92 | var flushed bool | 105 | var flushed bool | ||
| @@ -284,3 +297,253 @@ func isHVA(t reflect.Type) bool { | |||||
| 284 | return false | 297 | return false | ||
| 285 | } | 298 | } | ||
| 286 | } | 299 | } | ||
| 300 | + | ||||
| 301 | + | // copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback. | |||
| 302 | + | // This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs. | |||
| 303 | + | func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) { | |||
| 304 | + | if runtime.GOOS != "darwin" { | |||
| 305 | + | panic("purego: should only be called on darwin") | |||
| 306 | + | } | |||
| 307 | + | for offset := uintptr(0); offset < size; offset += 8 { | |||
| 308 | + | var chunk uintptr | |||
| 309 | + | remaining := size - offset | |||
| 310 | + | if remaining >= 8 { | |||
| 311 | + | chunk = *(*uintptr)(unsafe.Add(ptr, offset)) | |||
| 312 | + | } else { | |||
| 313 | + | // Read byte-by-byte to avoid reading beyond allocation | |||
| 314 | + | for i := uintptr(0); i < remaining; i++ { | |||
| 315 | + | b := *(*byte)(unsafe.Add(ptr, offset+i)) | |||
| 316 | + | chunk |= uintptr(b) << (i * 8) | |||
| 317 | + | } | |||
| 318 | + | } | |||
| 319 | + | addChunk(chunk) | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | ||||
| 323 | + | // placeRegisters implements Darwin ARM64 calling convention for struct arguments. | |||
| 324 | + | // | |||
| 325 | + | // For HFA/HVA structs, each element must go in a separate register (or stack slot for elements | |||
| 326 | + | // that don't fit in registers). We use placeRegistersArm64 for this. | |||
| 327 | + | // | |||
| 328 | + | // For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in | |||
| 329 | + | // 8-byte chunks, which works correctly for both register and stack placement. | |||
| 330 | + | func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |||
| 331 | + | if runtime.GOOS != "darwin" { | |||
| 332 | + | panic("purego: placeRegistersDarwin should only be called on darwin") | |||
| 333 | + | } | |||
| 334 | + | // Check if this is an HFA/HVA | |||
| 335 | + | hfa := isHFA(v.Type()) | |||
| 336 | + | hva := isHVA(v.Type()) | |||
| 337 | + | ||||
| 338 | + | // For HFA/HVA structs, use the standard ARM64 logic which places each element separately | |||
| 339 | + | if hfa || hva { | |||
| 340 | + | placeRegistersArm64(v, addFloat, addInt) | |||
| 341 | + | return | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | // For non-HFA/HVA structs, use byte-level copying | |||
| 345 | + | // If the value is not addressable, create an addressable copy | |||
| 346 | + | if !v.CanAddr() { | |||
| 347 | + | addressable := reflect.New(v.Type()).Elem() | |||
| 348 | + | addressable.Set(v) | |||
| 349 | + | v = addressable | |||
| 350 | + | } | |||
| 351 | + | ptr := unsafe.Pointer(v.Addr().Pointer()) | |||
| 352 | + | size := v.Type().Size() | |||
| 353 | + | copyStruct8ByteChunks(ptr, size, addInt) | |||
| 354 | + | } | |||
| 355 | + | ||||
| 356 | + | // shouldBundleStackArgs determines if we need to start C-style packing for | |||
| 357 | + | // Darwin ARM64 stack arguments. This happens when registers are exhausted. | |||
| 358 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |||
| 359 | + | if runtime.GOOS != "darwin" { | |||
| 360 | + | return false | |||
| 361 | + | } | |||
| 362 | + | ||||
| 363 | + | kind := v.Kind() | |||
| 364 | + | isFloat := kind == reflect.Float32 || kind == reflect.Float64 | |||
| 365 | + | isInt := !isFloat && kind != reflect.Struct | |||
| 366 | + | primitiveOnStack := | |||
| 367 | + | (isInt && numInts >= numOfIntegerRegisters()) || | |||
| 368 | + | (isFloat && numFloats >= numOfFloatRegisters) | |||
| 369 | + | if primitiveOnStack { | |||
| 370 | + | return true | |||
| 371 | + | } | |||
| 372 | + | if kind != reflect.Struct { | |||
| 373 | + | return false | |||
| 374 | + | } | |||
| 375 | + | hfa := isHFA(v.Type()) | |||
| 376 | + | hva := isHVA(v.Type()) | |||
| 377 | + | size := v.Type().Size() | |||
| 378 | + | eligible := hfa || hva || size <= 16 | |||
| 379 | + | if !eligible { | |||
| 380 | + | return false | |||
| 381 | + | } | |||
| 382 | + | ||||
| 383 | + | if hfa { | |||
| 384 | + | need := v.NumField() | |||
| 385 | + | return numFloats+need > numOfFloatRegisters | |||
| 386 | + | } | |||
| 387 | + | ||||
| 388 | + | if hva { | |||
| 389 | + | need := v.NumField() | |||
| 390 | + | return numInts+need > numOfIntegerRegisters() | |||
| 391 | + | } | |||
| 392 | + | ||||
| 393 | + | slotsNeeded := int((size + align8ByteMask) / align8ByteSize) | |||
| 394 | + | return numInts+slotsNeeded > numOfIntegerRegisters() | |||
| 395 | + | } | |||
| 396 | + | ||||
| 397 | + | // structFitsInRegisters determines if a struct can still fit in remaining | |||
| 398 | + | // registers, used during stack argument bundling to decide if a struct | |||
| 399 | + | // should go through normal register allocation or be bundled with stack args. | |||
| 400 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |||
| 401 | + | if runtime.GOOS != "darwin" { | |||
| 402 | + | panic("purego: structFitsInRegisters should only be called on darwin") | |||
| 403 | + | } | |||
| 404 | + | hfa := isHFA(val.Type()) | |||
| 405 | + | hva := isHVA(val.Type()) | |||
| 406 | + | size := val.Type().Size() | |||
| 407 | + | ||||
| 408 | + | if hfa { | |||
| 409 | + | // HFA: check if elements fit in float registers | |||
| 410 | + | if tempNumFloats+val.NumField() <= numOfFloatRegisters { | |||
| 411 | + | return true, tempNumInts, tempNumFloats + val.NumField() | |||
| 412 | + | } | |||
| 413 | + | } else if hva { | |||
| 414 | + | // HVA: check if elements fit in int registers | |||
| 415 | + | if tempNumInts+val.NumField() <= numOfIntegerRegisters() { | |||
| 416 | + | return true, tempNumInts + val.NumField(), tempNumFloats | |||
| 417 | + | } | |||
| 418 | + | } else if size <= 16 { | |||
| 419 | + | // Non-HFA/HVA small structs use int registers for byte-packing | |||
| 420 | + | slotsNeeded := int((size + align8ByteMask) / align8ByteSize) | |||
| 421 | + | if tempNumInts+slotsNeeded <= numOfIntegerRegisters() { | |||
| 422 | + | return true, tempNumInts + slotsNeeded, tempNumFloats | |||
| 423 | + | } | |||
| 424 | + | } | |||
| 425 | + | ||||
| 426 | + | return false, tempNumInts, tempNumFloats | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | // collectStackArgs separates remaining arguments into those that fit in registers vs those that go on stack. | |||
| 430 | + | // It returns the stack arguments and processes register arguments through addValue. | |||
| 431 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |||
| 432 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |||
| 433 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |||
| 434 | + | if runtime.GOOS != "darwin" { | |||
| 435 | + | panic("purego: collectStackArgs should only be called on darwin") | |||
| 436 | + | } | |||
| 437 | + | ||||
| 438 | + | var stackArgs []reflect.Value | |||
| 439 | + | tempNumInts := numInts | |||
| 440 | + | tempNumFloats := numFloats | |||
| 441 | + | ||||
| 442 | + | for j, val := range args[startIdx:] { | |||
| 443 | + | // Determine if this argument goes to register or stack | |||
| 444 | + | var fitsInRegister bool | |||
| 445 | + | var newNumInts, newNumFloats int | |||
| 446 | + | ||||
| 447 | + | if val.Kind() == reflect.Struct { | |||
| 448 | + | // Check if struct still fits in remaining registers | |||
| 449 | + | fitsInRegister, newNumInts, newNumFloats = structFitsInRegisters(val, tempNumInts, tempNumFloats) | |||
| 450 | + | } else { | |||
| 451 | + | // Primitive argument | |||
| 452 | + | isFloat := val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64 | |||
| 453 | + | if isFloat { | |||
| 454 | + | fitsInRegister = tempNumFloats < numOfFloatRegisters | |||
| 455 | + | newNumFloats = tempNumFloats + 1 | |||
| 456 | + | newNumInts = tempNumInts | |||
| 457 | + | } else { | |||
| 458 | + | fitsInRegister = tempNumInts < numOfIntegerRegisters() | |||
| 459 | + | newNumInts = tempNumInts + 1 | |||
| 460 | + | newNumFloats = tempNumFloats | |||
| 461 | + | } | |||
| 462 | + | } | |||
| 463 | + | ||||
| 464 | + | if fitsInRegister { | |||
| 465 | + | // Process through normal register allocation | |||
| 466 | + | tempNumInts = newNumInts | |||
| 467 | + | tempNumFloats = newNumFloats | |||
| 468 | + | keepAlive = addValue(val, keepAlive, addInt, addFloat, addStack, pNumInts, pNumFloats, pNumStack) | |||
| 469 | + | } else { | |||
| 470 | + | // Convert strings to C strings before bundling | |||
| 471 | + | if val.Kind() == reflect.String { | |||
| 472 | + | ptr := strings.CString(val.String()) | |||
| 473 | + | keepAlive = append(keepAlive, ptr) | |||
| 474 | + | val = reflect.ValueOf(ptr) | |||
| 475 | + | args[startIdx+j] = val | |||
| 476 | + | } | |||
| 477 | + | stackArgs = append(stackArgs, val) | |||
| 478 | + | } | |||
| 479 | + | } | |||
| 480 | + | ||||
| 481 | + | return stackArgs, keepAlive | |||
| 482 | + | } | |||
| 483 | + | ||||
| 484 | + | const ( | |||
| 485 | + | paddingFieldPrefix = "Pad" | |||
| 486 | + | ) | |||
| 487 | + | ||||
| 488 | + | // bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing. | |||
| 489 | + | // It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks. | |||
| 490 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |||
| 491 | + | if runtime.GOOS != "darwin" { | |||
| 492 | + | panic("purego: bundleStackArgs should only be called on darwin") | |||
| 493 | + | } | |||
| 494 | + | if len(stackArgs) == 0 { | |||
| 495 | + | return | |||
| 496 | + | } | |||
| 497 | + | ||||
| 498 | + | // Build struct fields with proper C alignment and padding | |||
| 499 | + | var fields []reflect.StructField | |||
| 500 | + | currentOffset := uintptr(0) | |||
| 501 | + | fieldIndex := 0 | |||
| 502 | + | ||||
| 503 | + | for j, val := range stackArgs { | |||
| 504 | + | valSize := val.Type().Size() | |||
| 505 | + | valAlign := val.Type().Align() | |||
| 506 | + | ||||
| 507 | + | // ARM64 requires 8-byte alignment for 8-byte or larger structs | |||
| 508 | + | if val.Kind() == reflect.Struct && valSize >= 8 { | |||
| 509 | + | valAlign = 8 | |||
| 510 | + | } | |||
| 511 | + | ||||
| 512 | + | // Add padding field if needed for alignment | |||
| 513 | + | if currentOffset%uintptr(valAlign) != 0 { | |||
| 514 | + | paddingNeeded := uintptr(valAlign) - (currentOffset % uintptr(valAlign)) | |||
| 515 | + | fields = append(fields, reflect.StructField{ | |||
| 516 | + | Name: paddingFieldPrefix + strconv.Itoa(fieldIndex), | |||
| 517 | + | Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeOf(byte(0))), | |||
| 518 | + | }) | |||
| 519 | + | currentOffset += paddingNeeded | |||
| 520 | + | fieldIndex++ | |||
| 521 | + | } | |||
| 522 | + | ||||
| 523 | + | fields = append(fields, reflect.StructField{ | |||
| 524 | + | Name: "X" + strconv.Itoa(j), | |||
| 525 | + | Type: val.Type(), | |||
| 526 | + | }) | |||
| 527 | + | currentOffset += valSize | |||
| 528 | + | fieldIndex++ | |||
| 529 | + | } | |||
| 530 | + | ||||
| 531 | + | // Create and populate the packed struct | |||
| 532 | + | structType := reflect.StructOf(fields) | |||
| 533 | + | structInstance := reflect.New(structType).Elem() | |||
| 534 | + | ||||
| 535 | + | // Set values (skip padding fields) | |||
| 536 | + | argIndex := 0 | |||
| 537 | + | for j := 0; j < structInstance.NumField(); j++ { | |||
| 538 | + | fieldName := structType.Field(j).Name | |||
| 539 | + | if stdstrings.HasPrefix(fieldName, paddingFieldPrefix) { | |||
| 540 | + | continue | |||
| 541 | + | } | |||
| 542 | + | structInstance.Field(j).Set(stackArgs[argIndex]) | |||
| 543 | + | argIndex++ | |||
| 544 | + | } | |||
| 545 | + | ||||
| 546 | + | ptr := unsafe.Pointer(structInstance.Addr().Pointer()) | |||
| 547 | + | size := structType.Size() | |||
| 548 | + | copyStruct8ByteChunks(ptr, size, addStack) | |||
| 549 | + | } | |||
M struct_loong64.go +23 -0
| @@ -188,3 +188,26 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any { | |||
| 188 | 188 | addInt(uintptr(ptr)) | |
| 189 | 189 | return keepAlive | |
| 190 | 190 | } | |
| 191 | + | ||
| 192 | + | // shouldBundleStackArgs always returns false on loong64 | |
| 193 | + | // since C-style stack argument bundling is only needed on Darwin ARM64. | |
| 194 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |
| 195 | + | return false | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | // structFitsInRegisters is not used on loong64. | |
| 199 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |
| 200 | + | panic("purego: structFitsInRegisters should not be called on loong64") | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // collectStackArgs is not used on loong64. | |
| 204 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |
| 205 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |
| 206 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |
| 207 | + | panic("purego: collectStackArgs should not be called on loong64") | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // bundleStackArgs is not used on loong64. | |
| 211 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |
| 212 | + | panic("purego: bundleStackArgs should not be called on loong64") | |
| 213 | + | } | |
| @@ -188,3 +188,26 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any { | |||||
| 188 | addInt(uintptr(ptr)) | 188 | addInt(uintptr(ptr)) | ||
| 189 | return keepAlive | 189 | return keepAlive | ||
| 190 | } | 190 | } | ||
| 191 | + | ||||
| 192 | + | // shouldBundleStackArgs always returns false on loong64 | |||
| 193 | + | // since C-style stack argument bundling is only needed on Darwin ARM64. | |||
| 194 | + | func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { | |||
| 195 | + | return false | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | // structFitsInRegisters is not used on loong64. | |||
| 199 | + | func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) { | |||
| 200 | + | panic("purego: structFitsInRegisters should not be called on loong64") | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // collectStackArgs is not used on loong64. | |||
| 204 | + | func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, | |||
| 205 | + | keepAlive []any, addInt, addFloat, addStack func(uintptr), | |||
| 206 | + | pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { | |||
| 207 | + | panic("purego: collectStackArgs should not be called on loong64") | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // bundleStackArgs is not used on loong64. | |||
| 211 | + | func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { | |||
| 212 | + | panic("purego: bundleStackArgs should not be called on loong64") | |||
| 213 | + | } | |||
D struct_other.go +0 -20
| @@ -1,20 +0,0 @@ | |||
| 1 | - | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | - | // SPDX-FileCopyrightText: 2024 The Ebitengine Authors | |
| 3 | - | ||
| 4 | - | //go:build !amd64 && !arm64 && !loong64 | |
| 5 | - | ||
| 6 | - | package purego | |
| 7 | - | ||
| 8 | - | import "reflect" | |
| 9 | - | ||
| 10 | - | func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { | |
| 11 | - | panic("purego: struct arguments are not supported") | |
| 12 | - | } | |
| 13 | - | ||
| 14 | - | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | |
| 15 | - | panic("purego: struct returns are not supported") | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |
| 19 | - | panic("purego: not needed on other platforms") | |
| 20 | - | } | |
| @@ -1,20 +0,0 @@ | |||||
| 1 | - | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | - | // SPDX-FileCopyrightText: 2024 The Ebitengine Authors | |||
| 3 | - | ||||
| 4 | - | //go:build !amd64 && !arm64 && !loong64 | |||
| 5 | - | ||||
| 6 | - | package purego | |||
| 7 | - | ||||
| 8 | - | import "reflect" | |||
| 9 | - | ||||
| 10 | - | func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { | |||
| 11 | - | panic("purego: struct arguments are not supported") | |||
| 12 | - | } | |||
| 13 | - | ||||
| 14 | - | func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { | |||
| 15 | - | panic("purego: struct returns are not supported") | |||
| 16 | - | } | |||
| 17 | - | ||||
| 18 | - | func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { | |||
| 19 | - | panic("purego: not needed on other platforms") | |||
| 20 | - | } | |||
A syscall_other.go +20 -0
| @@ -0,0 +1,20 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | //go:build linux && !amd64 && !arm64 && !loong64 | |
| 5 | + | ||
| 6 | + | package purego | |
| 7 | + | ||
| 8 | + | // getCallbackStart returns the start address of the callback region. | |
| 9 | + | // This platform doesn't support the callback detection mechanism. | |
| 10 | + | // TODO: Remove this function once callback tight packing is implemented. | |
| 11 | + | func getCallbackStart() uintptr { | |
| 12 | + | panic("purego: getCallbackStart not supported on this platform") | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | // getMaxCB returns the maximum number of callbacks. | |
| 16 | + | // This platform doesn't support the callback detection mechanism. | |
| 17 | + | // TODO: Remove this function once callback tight packing is implemented. | |
| 18 | + | func getMaxCB() int { | |
| 19 | + | panic("purego: getMaxCB not supported on this platform") | |
| 20 | + | } | |
| @@ -0,0 +1,20 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | //go:build linux && !amd64 && !arm64 && !loong64 | |||
| 5 | + | ||||
| 6 | + | package purego | |||
| 7 | + | ||||
| 8 | + | // getCallbackStart returns the start address of the callback region. | |||
| 9 | + | // This platform doesn't support the callback detection mechanism. | |||
| 10 | + | // TODO: Remove this function once callback tight packing is implemented. | |||
| 11 | + | func getCallbackStart() uintptr { | |||
| 12 | + | panic("purego: getCallbackStart not supported on this platform") | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | // getMaxCB returns the maximum number of callbacks. | |||
| 16 | + | // This platform doesn't support the callback detection mechanism. | |||
| 17 | + | // TODO: Remove this function once callback tight packing is implemented. | |||
| 18 | + | func getMaxCB() int { | |||
| 19 | + | panic("purego: getMaxCB not supported on this platform") | |||
| 20 | + | } | |||
M syscall_sysv.go +12 -0
| @@ -224,3 +224,15 @@ func callbackasmAddr(i int) uintptr { | |||
| 224 | 224 | } | |
| 225 | 225 | return callbackasmABI0 + uintptr(i*entrySize) | |
| 226 | 226 | } | |
| 227 | + | ||
| 228 | + | // getCallbackStart returns the start address of the callback region. | |
| 229 | + | // TODO: Remove this function once callback tight packing is implemented. | |
| 230 | + | func getCallbackStart() uintptr { | |
| 231 | + | return callbackasmABI0 | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | // getMaxCB returns the maximum number of callbacks. | |
| 235 | + | // TODO: Remove this function once callback tight packing is implemented. | |
| 236 | + | func getMaxCB() int { | |
| 237 | + | return maxCB | |
| 238 | + | } | |
| @@ -224,3 +224,15 @@ func callbackasmAddr(i int) uintptr { | |||||
| 224 | } | 224 | } | ||
| 225 | return callbackasmABI0 + uintptr(i*entrySize) | 225 | return callbackasmABI0 + uintptr(i*entrySize) | ||
| 226 | } | 226 | } | ||
| 227 | + | ||||
| 228 | + | // getCallbackStart returns the start address of the callback region. | |||
| 229 | + | // TODO: Remove this function once callback tight packing is implemented. | |||
| 230 | + | func getCallbackStart() uintptr { | |||
| 231 | + | return callbackasmABI0 | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | // getMaxCB returns the maximum number of callbacks. | |||
| 235 | + | // TODO: Remove this function once callback tight packing is implemented. | |||
| 236 | + | func getMaxCB() int { | |||
| 237 | + | return maxCB | |||
| 238 | + | } | |||
M syscall_windows.go +14 -0
| @@ -44,3 +44,17 @@ func NewCallback(fn any) uintptr { | |||
| 44 | 44 | func loadSymbol(handle uintptr, name string) (uintptr, error) { | |
| 45 | 45 | return syscall.GetProcAddress(syscall.Handle(handle), name) | |
| 46 | 46 | } | |
| 47 | + | ||
| 48 | + | // getCallbackStart returns the start address of the callback region. | |
| 49 | + | // On Windows, this should never be called as tight packing is not used. | |
| 50 | + | // TODO: Remove this function once callback tight packing is implemented. | |
| 51 | + | func getCallbackStart() uintptr { | |
| 52 | + | panic("purego: getCallbackStart should not be called on Windows") | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // getMaxCB returns the maximum number of callbacks. | |
| 56 | + | // On Windows, this should never be called as tight packing is not used. | |
| 57 | + | // TODO: Remove this function once callback tight packing is implemented. | |
| 58 | + | func getMaxCB() int { | |
| 59 | + | panic("purego: getMaxCB should not be called on Windows") | |
| 60 | + | } | |
| @@ -44,3 +44,17 @@ func NewCallback(fn any) uintptr { | |||||
| 44 | func loadSymbol(handle uintptr, name string) (uintptr, error) { | 44 | func loadSymbol(handle uintptr, name string) (uintptr, error) { | ||
| 45 | return syscall.GetProcAddress(syscall.Handle(handle), name) | 45 | return syscall.GetProcAddress(syscall.Handle(handle), name) | ||
| 46 | } | 46 | } | ||
| 47 | + | ||||
| 48 | + | // getCallbackStart returns the start address of the callback region. | |||
| 49 | + | // On Windows, this should never be called as tight packing is not used. | |||
| 50 | + | // TODO: Remove this function once callback tight packing is implemented. | |||
| 51 | + | func getCallbackStart() uintptr { | |||
| 52 | + | panic("purego: getCallbackStart should not be called on Windows") | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // getMaxCB returns the maximum number of callbacks. | |||
| 56 | + | // On Windows, this should never be called as tight packing is not used. | |||
| 57 | + | // TODO: Remove this function once callback tight packing is implemented. | |||
| 58 | + | func getMaxCB() int { | |||
| 59 | + | panic("purego: getMaxCB should not be called on Windows") | |||
| 60 | + | } | |||
M testdata/abitest/abi_test.c +102 -2
| @@ -1,8 +1,10 @@ | |||
| 1 | 1 | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | 2 | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | |
| 3 | 3 | ||
| 4 | - | #include <stdint.h> | |
| 5 | 4 | #include <assert.h> | |
| 5 | + | #include <inttypes.h> | |
| 6 | + | #include <stdbool.h> | |
| 7 | + | #include <stdint.h> | |
| 6 | 8 | #include <stdio.h> | |
| 7 | 9 | #include <string.h> | |
| 8 | 10 | ||
| @@ -10,7 +12,7 @@ uint32_t stack_uint8_t(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t | |||
| 10 | 12 | assert(i == 1); | |
| 11 | 13 | assert(j == 2); | |
| 12 | 14 | assert(k == 1024); | |
| 13 | - | return a | b | c | d | e | f | g | h | (uint32_t) i | (uint32_t) j | k; | |
| 15 | + | return a | b | c | d | e | f | g | h | (uint32_t)i | (uint32_t)j | k; | |
| 14 | 16 | } | |
| 15 | 17 | ||
| 16 | 18 | uint32_t reg_uint8_t(uint8_t a, uint8_t b, uint32_t c) { | |
| @@ -29,3 +31,101 @@ uint32_t stack_string(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e | |||
| 29 | 31 | void stack_8i32_3strings(char* result, size_t size, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char* s1, const char* s2, const char* s3) { | |
| 30 | 32 | snprintf(result, size, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%s:%s", a1, a2, a3, a4, a5, a6, a7, a8, s1, s2, s3); | |
| 31 | 33 | } | |
| 34 | + | ||
| 35 | + | // HFA (Homogeneous Float Aggregate) struct with 2 floats | |
| 36 | + | typedef struct { | |
| 37 | + | float x; | |
| 38 | + | float y; | |
| 39 | + | } Float2; | |
| 40 | + | ||
| 41 | + | // HFA struct with 4 floats | |
| 42 | + | typedef struct { | |
| 43 | + | float x; | |
| 44 | + | float y; | |
| 45 | + | float z; | |
| 46 | + | float w; | |
| 47 | + | } Float4; | |
| 48 | + | ||
| 49 | + | // Non-HFA struct (mixed types) | |
| 50 | + | typedef struct { | |
| 51 | + | int32_t a; | |
| 52 | + | float b; | |
| 53 | + | } MixedStruct; | |
| 54 | + | ||
| 55 | + | // Small struct that fits in one register | |
| 56 | + | typedef struct { | |
| 57 | + | int32_t x; | |
| 58 | + | int32_t y; | |
| 59 | + | } IntPair; | |
| 60 | + | ||
| 61 | + | // Test: 8 int registers exhausted, then HFA struct on stack | |
| 62 | + | void stack_8int_hfa2_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float2 f) { | |
| 63 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f", | |
| 64 | + | a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y); | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // Test: 8 int registers exhausted, then multiple structs on stack | |
| 68 | + | void stack_8int_2structs_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, IntPair p1, IntPair p2) { | |
| 69 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |
| 70 | + | a1, a2, a3, a4, a5, a6, a7, a8, p1.x, p1.y, p2.x, p2.y); | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // Test: 8 float registers exhausted, then HFA on stack | |
| 74 | + | void stack_8float_hfa2_stack(char *buf, size_t bufsize, float f1, float f2, float f3, float f4, float f5, float f6, float f7, float f8, Float2 f) { | |
| 75 | + | snprintf(buf, bufsize, "%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f", | |
| 76 | + | f1, f2, f3, f4, f5, f6, f7, f8, f.x, f.y); | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // Test: mixed - int regs exhausted, float struct can still use float regs | |
| 80 | + | void stack_8int_hfa2_floatregs(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float2 f) { | |
| 81 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f", | |
| 82 | + | a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y); | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // Test: primitives and struct interleaved on stack | |
| 86 | + | void stack_8int_int_struct_int(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, IntPair p, int32_t a10) { | |
| 87 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |
| 88 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, p.x, p.y, a10); | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // Test: HFA4 struct on stack (4 floats) | |
| 92 | + | void stack_8int_hfa4_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float4 f) { | |
| 93 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f:%.1f:%.1f", | |
| 94 | + | a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y, f.z, f.w); | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // Test: mixed type struct on stack | |
| 98 | + | void stack_8int_mixed_struct(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, MixedStruct m) { | |
| 99 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%.1f", | |
| 100 | + | a1, a2, a3, a4, a5, a6, a7, a8, m.a, m.b); | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | void stack_10_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10) { | |
| 104 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |
| 105 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, a10); | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | void stack_11_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10, int32_t a11) { | |
| 109 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |
| 110 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11); | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | void stack_10_float32(char *buf, size_t bufsize, float f1, float f2, float f3, float f4, float f5, float f6, float f7, float f8, float f9, float f10) { | |
| 114 | + | snprintf(buf, bufsize, "%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f", | |
| 115 | + | f1, f2, f3, f4, f5, f6, f7, f8, f9, f10); | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | void stack_mixed_stack_4args(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char *s1, bool b1, int32_t a9, const char *s2) { | |
| 119 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%d:%d:%s", | |
| 120 | + | a1, a2, a3, a4, a5, a6, a7, a8, s1, b1, a9, s2); | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | void stack_20_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10, int32_t a11, int32_t a12, int32_t a13, int32_t a14, int32_t a15, int32_t a16, int32_t a17, int32_t a18, int32_t a19, int32_t a20) { | |
| 124 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |
| 125 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20); | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | void stack_25_int64_exceeds(char *buf, size_t bufsize, int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10, int64_t a11, int64_t a12, int64_t a13, int64_t a14, int64_t a15, int64_t a16, int64_t a17, int64_t a18, int64_t a19, int64_t a20, int64_t a21, int64_t a22, int64_t a23, int64_t a24, int64_t a25) { | |
| 129 | + | snprintf(buf, bufsize, "%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64, | |
| 130 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25); | |
| 131 | + | } | |
| @@ -1,8 +1,10 @@ | |||||
| 1 | // SPDX-License-Identifier: Apache-2.0 | 1 | // SPDX-License-Identifier: Apache-2.0 | ||
| 2 | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | 2 | // SPDX-FileCopyrightText: 2025 The Ebitengine Authors | ||
| 3 | 3 | ||||
| 4 | - | #include <stdint.h> | |||
| 5 | #include <assert.h> | 4 | #include <assert.h> | ||
| 5 | + | #include <inttypes.h> | |||
| 6 | + | #include <stdbool.h> | |||
| 7 | + | #include <stdint.h> | |||
| 6 | #include <stdio.h> | 8 | #include <stdio.h> | ||
| 7 | #include <string.h> | 9 | #include <string.h> | ||
| 8 | 10 | ||||
| @@ -10,7 +12,7 @@ uint32_t stack_uint8_t(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t | |||||
| 10 | assert(i == 1); | 12 | assert(i == 1); | ||
| 11 | assert(j == 2); | 13 | assert(j == 2); | ||
| 12 | assert(k == 1024); | 14 | assert(k == 1024); | ||
| 13 | - | return a | b | c | d | e | f | g | h | (uint32_t) i | (uint32_t) j | k; | 15 | + | return a | b | c | d | e | f | g | h | (uint32_t)i | (uint32_t)j | k; |
| 14 | } | 16 | } | ||
| 15 | 17 | ||||
| 16 | uint32_t reg_uint8_t(uint8_t a, uint8_t b, uint32_t c) { | 18 | uint32_t reg_uint8_t(uint8_t a, uint8_t b, uint32_t c) { | ||
| @@ -29,3 +31,101 @@ uint32_t stack_string(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e | |||||
| 29 | void stack_8i32_3strings(char* result, size_t size, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char* s1, const char* s2, const char* s3) { | 31 | void stack_8i32_3strings(char* result, size_t size, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char* s1, const char* s2, const char* s3) { | ||
| 30 | snprintf(result, size, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%s:%s", a1, a2, a3, a4, a5, a6, a7, a8, s1, s2, s3); | 32 | snprintf(result, size, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%s:%s", a1, a2, a3, a4, a5, a6, a7, a8, s1, s2, s3); | ||
| 31 | } | 33 | } | ||
| 34 | + | ||||
| 35 | + | // HFA (Homogeneous Float Aggregate) struct with 2 floats | |||
| 36 | + | typedef struct { | |||
| 37 | + | float x; | |||
| 38 | + | float y; | |||
| 39 | + | } Float2; | |||
| 40 | + | ||||
| 41 | + | // HFA struct with 4 floats | |||
| 42 | + | typedef struct { | |||
| 43 | + | float x; | |||
| 44 | + | float y; | |||
| 45 | + | float z; | |||
| 46 | + | float w; | |||
| 47 | + | } Float4; | |||
| 48 | + | ||||
| 49 | + | // Non-HFA struct (mixed types) | |||
| 50 | + | typedef struct { | |||
| 51 | + | int32_t a; | |||
| 52 | + | float b; | |||
| 53 | + | } MixedStruct; | |||
| 54 | + | ||||
| 55 | + | // Small struct that fits in one register | |||
| 56 | + | typedef struct { | |||
| 57 | + | int32_t x; | |||
| 58 | + | int32_t y; | |||
| 59 | + | } IntPair; | |||
| 60 | + | ||||
| 61 | + | // Test: 8 int registers exhausted, then HFA struct on stack | |||
| 62 | + | void stack_8int_hfa2_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float2 f) { | |||
| 63 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f", | |||
| 64 | + | a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y); | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // Test: 8 int registers exhausted, then multiple structs on stack | |||
| 68 | + | void stack_8int_2structs_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, IntPair p1, IntPair p2) { | |||
| 69 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |||
| 70 | + | a1, a2, a3, a4, a5, a6, a7, a8, p1.x, p1.y, p2.x, p2.y); | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // Test: 8 float registers exhausted, then HFA on stack | |||
| 74 | + | void stack_8float_hfa2_stack(char *buf, size_t bufsize, float f1, float f2, float f3, float f4, float f5, float f6, float f7, float f8, Float2 f) { | |||
| 75 | + | snprintf(buf, bufsize, "%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f", | |||
| 76 | + | f1, f2, f3, f4, f5, f6, f7, f8, f.x, f.y); | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // Test: mixed - int regs exhausted, float struct can still use float regs | |||
| 80 | + | void stack_8int_hfa2_floatregs(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float2 f) { | |||
| 81 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f", | |||
| 82 | + | a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y); | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // Test: primitives and struct interleaved on stack | |||
| 86 | + | void stack_8int_int_struct_int(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, IntPair p, int32_t a10) { | |||
| 87 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |||
| 88 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, p.x, p.y, a10); | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // Test: HFA4 struct on stack (4 floats) | |||
| 92 | + | void stack_8int_hfa4_stack(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, Float4 f) { | |||
| 93 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%.1f:%.1f:%.1f:%.1f", | |||
| 94 | + | a1, a2, a3, a4, a5, a6, a7, a8, f.x, f.y, f.z, f.w); | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // Test: mixed type struct on stack | |||
| 98 | + | void stack_8int_mixed_struct(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, MixedStruct m) { | |||
| 99 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%.1f", | |||
| 100 | + | a1, a2, a3, a4, a5, a6, a7, a8, m.a, m.b); | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | void stack_10_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10) { | |||
| 104 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |||
| 105 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, a10); | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | void stack_11_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10, int32_t a11) { | |||
| 109 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |||
| 110 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11); | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | void stack_10_float32(char *buf, size_t bufsize, float f1, float f2, float f3, float f4, float f5, float f6, float f7, float f8, float f9, float f10) { | |||
| 114 | + | snprintf(buf, bufsize, "%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f:%.1f", | |||
| 115 | + | f1, f2, f3, f4, f5, f6, f7, f8, f9, f10); | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | void stack_mixed_stack_4args(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, const char *s1, bool b1, int32_t a9, const char *s2) { | |||
| 119 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%s:%d:%d:%s", | |||
| 120 | + | a1, a2, a3, a4, a5, a6, a7, a8, s1, b1, a9, s2); | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | void stack_20_int32(char *buf, size_t bufsize, int32_t a1, int32_t a2, int32_t a3, int32_t a4, int32_t a5, int32_t a6, int32_t a7, int32_t a8, int32_t a9, int32_t a10, int32_t a11, int32_t a12, int32_t a13, int32_t a14, int32_t a15, int32_t a16, int32_t a17, int32_t a18, int32_t a19, int32_t a20) { | |||
| 124 | + | snprintf(buf, bufsize, "%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d", | |||
| 125 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20); | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | void stack_25_int64_exceeds(char *buf, size_t bufsize, int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10, int64_t a11, int64_t a12, int64_t a13, int64_t a14, int64_t a15, int64_t a16, int64_t a17, int64_t a18, int64_t a19, int64_t a20, int64_t a21, int64_t a22, int64_t a23, int64_t a24, int64_t a25) { | |||
| 129 | + | snprintf(buf, bufsize, "%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64, | |||
| 130 | + | a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25); | |||
| 131 | + | } | |||