func.go raw

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