feat: FFI optimizations for high-frequency callback/function dispatch
- Fix []string argument marshalling (addValue: []string → NULL-terminated char**) - Fix []string return value handling (char** → []string) - Replace callback mutex with RWMutex (append-only array, reads safe without exclusive lock) - Pre-allocate callback argument buffers via sync.Pool (eliminate per-call make) - Fast path for pointer-only RegisterFunc calls (skip keepAlive/defer overhead) - Add SyscallSelf helper for zero-alloc method-style calls - Eliminate return value heap allocation for scalar types (direct reflect.ValueOf) - Support reflect.Array arguments in addValue - Add structs.HostLayout to syscall15Args and callbackArgs (ABI safety)
- commit
- d2ade5cff9dd439a48d943f7ae6c015d8e79797b
- author
- brice · · 2026-03-23 09:46 UTC
- parent
- 386f7c8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +397 -107
M func.go +230 -99
| @@ -160,7 +160,7 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||
| 160 | 160 | } | |
| 161 | 161 | case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, | |
| 162 | 162 | reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer, | |
| 163 | - | reflect.Slice, reflect.Bool: | |
| 163 | + | reflect.Slice, reflect.Array, reflect.Bool: | |
| 164 | 164 | if ints < numOfIntegerRegisters() { | |
| 165 | 165 | ints++ | |
| 166 | 166 | } else { | |
| @@ -218,6 +218,28 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||
| 218 | 218 | } | |
| 219 | 219 | } | |
| 220 | 220 | ||
| 221 | + | // Task 3: Analyze types at RegisterFunc time to determine if keepAlive is needed. | |
| 222 | + | // keepAlive is only used for: string args (CString alloc), struct args (addStruct), | |
| 223 | + | // struct returns > maxRegAllocStructSize (hidden pointer), array args (addressable copy), | |
| 224 | + | // and variadic []any args (unknown types at registration time). | |
| 225 | + | needsKeepAlive := false | |
| 226 | + | for i := 0; i < ty.NumIn(); i++ { | |
| 227 | + | switch ty.In(i).Kind() { | |
| 228 | + | case reflect.String, reflect.Struct, reflect.Array: | |
| 229 | + | needsKeepAlive = true | |
| 230 | + | case reflect.Interface: | |
| 231 | + | // variadic []any — could contain anything | |
| 232 | + | needsKeepAlive = true | |
| 233 | + | } | |
| 234 | + | } | |
| 235 | + | if ty.NumOut() == 1 { | |
| 236 | + | switch ty.Out(0).Kind() { | |
| 237 | + | case reflect.Struct: | |
| 238 | + | // struct returns may need hidden pointer allocation stored in keepAlive | |
| 239 | + | needsKeepAlive = true | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | ||
| 221 | 243 | v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | |
| 222 | 244 | var sysargs [maxArgs]uintptr | |
| 223 | 245 | // Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free, | |
| @@ -264,132 +286,212 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||
| 264 | 286 | addFloat = addStack | |
| 265 | 287 | } | |
| 266 | 288 | ||
| 267 | - | var keepAlive []any | |
| 268 | - | defer func() { | |
| 269 | - | runtime.KeepAlive(keepAlive) | |
| 270 | - | runtime.KeepAlive(args) | |
| 271 | - | }() | |
| 289 | + | if needsKeepAlive { | |
| 290 | + | // Slow path: allocate keepAlive slice and defer cleanup. | |
| 291 | + | // Used when args include strings, structs, arrays, or variadic []any. | |
| 292 | + | var keepAlive []any | |
| 293 | + | defer func() { | |
| 294 | + | runtime.KeepAlive(keepAlive) | |
| 295 | + | runtime.KeepAlive(args) | |
| 296 | + | }() | |
| 272 | 297 | ||
| 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 { | |
| 298 | + | var arm64_r8 uintptr | |
| 299 | + | if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct { | |
| 300 | + | outType := ty.Out(0) | |
| 301 | + | if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize { | |
| 283 | 302 | val := reflect.New(outType) | |
| 284 | 303 | keepAlive = append(keepAlive, val) | |
| 285 | - | arm64_r8 = val.Pointer() | |
| 304 | + | addInt(val.Pointer()) | |
| 305 | + | } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize { | |
| 306 | + | isAllFloats, numFields := isAllSameFloat(outType) | |
| 307 | + | if !isAllFloats || numFields > 4 { | |
| 308 | + | val := reflect.New(outType) | |
| 309 | + | keepAlive = append(keepAlive, val) | |
| 310 | + | arm64_r8 = val.Pointer() | |
| 311 | + | } | |
| 312 | + | } | |
| 313 | + | } | |
| 314 | + | for i, v := range args { | |
| 315 | + | if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok { | |
| 316 | + | if i != len(args)-1 { | |
| 317 | + | panic("purego: can only expand last parameter") | |
| 318 | + | } | |
| 319 | + | for _, x := range variadic { | |
| 320 | + | keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 321 | + | } | |
| 322 | + | continue | |
| 323 | + | } | |
| 324 | + | // Check if we need to start Darwin ARM64 C-style stack packing | |
| 325 | + | if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) { | |
| 326 | + | // Collect and separate remaining args into register vs stack | |
| 327 | + | stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats, | |
| 328 | + | keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 329 | + | keepAlive = newKeepAlive | |
| 330 | + | ||
| 331 | + | // Bundle stack arguments with C-style packing | |
| 332 | + | bundleStackArgs(stackArgs, addStack) | |
| 333 | + | break | |
| 286 | 334 | } | |
| 335 | + | keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 287 | 336 | } | |
| 337 | + | return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, arm64_r8, args) | |
| 288 | 338 | } | |
| 339 | + | ||
| 340 | + | // Fast path: no keepAlive needed (pointer/integer/bool/float args only). | |
| 341 | + | // Skip keepAlive slice allocation and defer overhead. | |
| 342 | + | defer runtime.KeepAlive(args) | |
| 343 | + | ||
| 289 | 344 | for i, v := range args { | |
| 290 | 345 | if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok { | |
| 291 | 346 | if i != len(args)-1 { | |
| 292 | 347 | panic("purego: can only expand last parameter") | |
| 293 | 348 | } | |
| 294 | 349 | for _, x := range variadic { | |
| 295 | - | keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 350 | + | addValue(reflect.ValueOf(x), nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 296 | 351 | } | |
| 297 | 352 | continue | |
| 298 | 353 | } | |
| 299 | 354 | // Check if we need to start Darwin ARM64 C-style stack packing | |
| 300 | 355 | 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 | |
| 356 | + | stackArgs, _ := collectStackArgs(args, i, numInts, numFloats, | |
| 357 | + | nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 307 | 358 | bundleStackArgs(stackArgs, addStack) | |
| 308 | 359 | break | |
| 309 | 360 | } | |
| 310 | - | keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 361 | + | addValue(v, nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |
| 311 | 362 | } | |
| 312 | 363 | ||
| 313 | - | syscall := thePool.Get().(*syscall15Args) | |
| 314 | - | defer thePool.Put(syscall) | |
| 364 | + | return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, 0, args) | |
| 365 | + | }) | |
| 366 | + | fn.Set(v) | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | // callAndReturn performs the actual C function call and constructs the return value. | |
| 370 | + | // It is shared between the fast path (no keepAlive) and slow path (with keepAlive). | |
| 371 | + | func callAndReturn(ty reflect.Type, is32bit bool, cfn uintptr, sysargs *[maxArgs]uintptr, floats *[maxArgs]uintptr, arm64_r8 uintptr, args []reflect.Value) []reflect.Value { | |
| 372 | + | syscall := thePool.Get().(*syscall15Args) | |
| 373 | + | defer thePool.Put(syscall) | |
| 315 | 374 | ||
| 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 | |
| 375 | + | if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" { | |
| 376 | + | syscall.Set(cfn, sysargs[:], floats[:], 0) | |
| 377 | + | runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall)) | |
| 378 | + | } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { | |
| 379 | + | // Use the normal arm64 calling convention even on Windows | |
| 380 | + | syscall.Set(cfn, sysargs[:], floats[:], arm64_r8) | |
| 381 | + | runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall)) | |
| 382 | + | } else { | |
| 383 | + | *syscall = syscall15Args{} | |
| 384 | + | // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats | |
| 385 | + | syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], | |
| 386 | + | sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11], | |
| 387 | + | sysargs[12], sysargs[13], sysargs[14]) | |
| 388 | + | syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support | |
| 389 | + | } | |
| 390 | + | if ty.NumOut() == 0 { | |
| 391 | + | return nil | |
| 392 | + | } | |
| 393 | + | outType := ty.Out(0) | |
| 394 | + | // Task 5: For simple scalar return types, construct the reflect.Value directly | |
| 395 | + | // instead of allocating via reflect.New(outType).Elem(). | |
| 396 | + | // reflect.ValueOf for scalar types avoids the heap allocation that reflect.New incurs. | |
| 397 | + | var v reflect.Value | |
| 398 | + | switch outType.Kind() { | |
| 399 | + | case reflect.Uintptr: | |
| 400 | + | v = reflect.ValueOf(uintptr(syscall.a1)) | |
| 401 | + | case reflect.Uint: | |
| 402 | + | v = reflect.ValueOf(uint(syscall.a1)) | |
| 403 | + | case reflect.Uint8: | |
| 404 | + | v = reflect.ValueOf(uint8(syscall.a1)) | |
| 405 | + | case reflect.Uint16: | |
| 406 | + | v = reflect.ValueOf(uint16(syscall.a1)) | |
| 407 | + | case reflect.Uint32: | |
| 408 | + | v = reflect.ValueOf(uint32(syscall.a1)) | |
| 409 | + | case reflect.Uint64: | |
| 410 | + | v = reflect.ValueOf(uint64(syscall.a1)) | |
| 411 | + | case reflect.Int: | |
| 412 | + | v = reflect.ValueOf(int(syscall.a1)) | |
| 413 | + | case reflect.Int8: | |
| 414 | + | v = reflect.ValueOf(int8(syscall.a1)) | |
| 415 | + | case reflect.Int16: | |
| 416 | + | v = reflect.ValueOf(int16(syscall.a1)) | |
| 417 | + | case reflect.Int32: | |
| 418 | + | v = reflect.ValueOf(int32(syscall.a1)) | |
| 419 | + | case reflect.Int64: | |
| 420 | + | v = reflect.ValueOf(int64(syscall.a1)) | |
| 421 | + | case reflect.Bool: | |
| 422 | + | v = reflect.ValueOf(byte(syscall.a1) != 0) | |
| 423 | + | case reflect.UnsafePointer: | |
| 424 | + | // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer | |
| 425 | + | v = reflect.New(outType).Elem() | |
| 426 | + | v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))) | |
| 427 | + | case reflect.Pointer: | |
| 428 | + | v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem() | |
| 429 | + | case reflect.Func: | |
| 430 | + | // wrap this C function in a nicely typed Go function | |
| 431 | + | v = reflect.New(outType) | |
| 432 | + | RegisterFunc(v.Interface(), syscall.a1) | |
| 433 | + | case reflect.String: | |
| 434 | + | v = reflect.ValueOf(strings.GoString(syscall.a1)) | |
| 435 | + | case reflect.Float32: | |
| 436 | + | // NOTE: syscall.r2 is only the floating return value on 64bit platforms. | |
| 437 | + | // On 32bit platforms syscall.r2 is the upper part of a 64bit return. | |
| 438 | + | // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert. | |
| 439 | + | // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64. | |
| 440 | + | // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register. | |
| 441 | + | v = reflect.New(outType).Elem() | |
| 442 | + | switch runtime.GOARCH { | |
| 443 | + | case "386": | |
| 444 | + | v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32))) | |
| 445 | + | case "ppc64le": | |
| 446 | + | v.SetFloat(math.Float64frombits(uint64(syscall.f1))) | |
| 447 | + | case "s390x": | |
| 448 | + | // S390X is big-endian: float32 in upper 32 bits of 64-bit register | |
| 449 | + | v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32)))) | |
| 450 | + | default: | |
| 451 | + | v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1)))) | |
| 330 | 452 | } | |
| 331 | - | if ty.NumOut() == 0 { | |
| 332 | - | return nil | |
| 453 | + | case reflect.Float64: | |
| 454 | + | // NOTE: syscall.r2 is only the floating return value on 64bit platforms. | |
| 455 | + | // On 32bit platforms syscall.r2 is the upper part of a 64bit return. | |
| 456 | + | v = reflect.New(outType).Elem() | |
| 457 | + | if is32bit { | |
| 458 | + | v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32))) | |
| 459 | + | } else { | |
| 460 | + | v.SetFloat(math.Float64frombits(uint64(syscall.f1))) | |
| 333 | 461 | } | |
| 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.Pointer: | |
| 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))) | |
| 462 | + | case reflect.Struct: | |
| 463 | + | v = getStruct(outType, *syscall) | |
| 464 | + | case reflect.Slice: | |
| 465 | + | if outType.Elem().Kind() == reflect.String { | |
| 466 | + | // C returns char** (NULL-terminated array of strings) | |
| 467 | + | if syscall.a1 == 0 { | |
| 468 | + | v = reflect.Zero(outType) | |
| 376 | 469 | } else { | |
| 377 | - | v.SetFloat(math.Float64frombits(uint64(syscall.f1))) | |
| 470 | + | var result []string | |
| 471 | + | ptr := syscall.a1 | |
| 472 | + | for { | |
| 473 | + | strPtr := *(*uintptr)(unsafe.Pointer(ptr)) | |
| 474 | + | if strPtr == 0 { | |
| 475 | + | break | |
| 476 | + | } | |
| 477 | + | result = append(result, strings.GoString(strPtr)) | |
| 478 | + | ptr += unsafe.Sizeof(uintptr(0)) | |
| 479 | + | } | |
| 480 | + | v = reflect.ValueOf(result) | |
| 378 | 481 | } | |
| 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 | 482 | } else { | |
| 389 | - | return []reflect.Value{v} | |
| 483 | + | panic("purego: unsupported return slice element kind: " + outType.Elem().Kind().String()) | |
| 390 | 484 | } | |
| 391 | - | }) | |
| 392 | - | fn.Set(v) | |
| 485 | + | default: | |
| 486 | + | panic("purego: unsupported return kind: " + outType.Kind().String()) | |
| 487 | + | } | |
| 488 | + | if len(args) > 0 { | |
| 489 | + | // reuse args slice instead of allocating one when possible | |
| 490 | + | args[0] = v | |
| 491 | + | return args[:1] | |
| 492 | + | } else { | |
| 493 | + | return []reflect.Value{v} | |
| 494 | + | } | |
| 393 | 495 | } | |
| 394 | 496 | ||
| 395 | 497 | 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 { | |
| @@ -403,9 +505,27 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat | |||
| 403 | 505 | addInt(uintptr(v.Uint())) | |
| 404 | 506 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |
| 405 | 507 | addInt(uintptr(v.Int())) | |
| 406 | - | case reflect.Pointer, reflect.UnsafePointer, reflect.Slice: | |
| 508 | + | case reflect.Pointer, reflect.UnsafePointer: | |
| 407 | 509 | // There is no need to keepAlive this pointer separately because it is kept alive in the args variable | |
| 408 | 510 | addInt(v.Pointer()) | |
| 511 | + | case reflect.Slice: | |
| 512 | + | if v.Type().Elem().Kind() == reflect.String { | |
| 513 | + | ss := v.Interface().([]string) | |
| 514 | + | if ss == nil { | |
| 515 | + | addInt(0) | |
| 516 | + | } else { | |
| 517 | + | res := make([]*byte, len(ss)+1) | |
| 518 | + | for i, s := range ss { | |
| 519 | + | res[i] = strings.CString(s) | |
| 520 | + | } | |
| 521 | + | // NULL-terminated for C | |
| 522 | + | res[len(ss)] = nil | |
| 523 | + | keepAlive = append(keepAlive, res) | |
| 524 | + | addInt(uintptr(unsafe.Pointer(&res[0]))) | |
| 525 | + | } | |
| 526 | + | } else { | |
| 527 | + | addInt(v.Pointer()) | |
| 528 | + | } | |
| 409 | 529 | case reflect.Func: | |
| 410 | 530 | addInt(NewCallback(v.Interface())) | |
| 411 | 531 | case reflect.Bool: | |
| @@ -429,6 +549,17 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat | |||
| 429 | 549 | } else { | |
| 430 | 550 | addFloat(uintptr(math.Float64bits(v.Float()))) | |
| 431 | 551 | } | |
| 552 | + | case reflect.Array: | |
| 553 | + | // Task 7: Support fixed-size array arguments by passing a pointer to the array data. | |
| 554 | + | if v.CanAddr() { | |
| 555 | + | addInt(v.Addr().Pointer()) | |
| 556 | + | } else { | |
| 557 | + | // Array is not addressable (e.g. passed by value); make an addressable copy. | |
| 558 | + | tmp := reflect.New(v.Type()).Elem() | |
| 559 | + | tmp.Set(v) | |
| 560 | + | keepAlive = append(keepAlive, tmp) | |
| 561 | + | addInt(tmp.Addr().Pointer()) | |
| 562 | + | } | |
| 432 | 563 | case reflect.Struct: | |
| 433 | 564 | keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive) | |
| 434 | 565 | default: | |
| @@ -160,7 +160,7 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||||
| 160 | } | 160 | } | ||
| 161 | case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, | 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.Pointer, reflect.UnsafePointer, | 162 | reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer, | ||
| 163 | - | reflect.Slice, reflect.Bool: | 163 | + | reflect.Slice, reflect.Array, reflect.Bool: |
| 164 | if ints < numOfIntegerRegisters() { | 164 | if ints < numOfIntegerRegisters() { | ||
| 165 | ints++ | 165 | ints++ | ||
| 166 | } else { | 166 | } else { | ||
| @@ -218,6 +218,28 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||||
| 218 | } | 218 | } | ||
| 219 | } | 219 | } | ||
| 220 | 220 | ||||
| 221 | + | // Task 3: Analyze types at RegisterFunc time to determine if keepAlive is needed. | |||
| 222 | + | // keepAlive is only used for: string args (CString alloc), struct args (addStruct), | |||
| 223 | + | // struct returns > maxRegAllocStructSize (hidden pointer), array args (addressable copy), | |||
| 224 | + | // and variadic []any args (unknown types at registration time). | |||
| 225 | + | needsKeepAlive := false | |||
| 226 | + | for i := 0; i < ty.NumIn(); i++ { | |||
| 227 | + | switch ty.In(i).Kind() { | |||
| 228 | + | case reflect.String, reflect.Struct, reflect.Array: | |||
| 229 | + | needsKeepAlive = true | |||
| 230 | + | case reflect.Interface: | |||
| 231 | + | // variadic []any — could contain anything | |||
| 232 | + | needsKeepAlive = true | |||
| 233 | + | } | |||
| 234 | + | } | |||
| 235 | + | if ty.NumOut() == 1 { | |||
| 236 | + | switch ty.Out(0).Kind() { | |||
| 237 | + | case reflect.Struct: | |||
| 238 | + | // struct returns may need hidden pointer allocation stored in keepAlive | |||
| 239 | + | needsKeepAlive = true | |||
| 240 | + | } | |||
| 241 | + | } | |||
| 242 | + | ||||
| 221 | v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | 243 | v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | ||
| 222 | var sysargs [maxArgs]uintptr | 244 | var sysargs [maxArgs]uintptr | ||
| 223 | // Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free, | 245 | // Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free, | ||
| @@ -264,132 +286,212 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||||
| 264 | addFloat = addStack | 286 | addFloat = addStack | ||
| 265 | } | 287 | } | ||
| 266 | 288 | ||||
| 267 | - | var keepAlive []any | 289 | + | if needsKeepAlive { |
| 268 | - | defer func() { | 290 | + | // Slow path: allocate keepAlive slice and defer cleanup. |
| 269 | - | runtime.KeepAlive(keepAlive) | 291 | + | // Used when args include strings, structs, arrays, or variadic []any. |
| 270 | - | runtime.KeepAlive(args) | 292 | + | var keepAlive []any |
| 271 | - | }() | 293 | + | defer func() { |
| 294 | + | runtime.KeepAlive(keepAlive) | |||
| 295 | + | runtime.KeepAlive(args) | |||
| 296 | + | }() | |||
| 272 | 297 | ||||
| 273 | - | var arm64_r8 uintptr | 298 | + | var arm64_r8 uintptr |
| 274 | - | if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct { | 299 | + | if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct { |
| 275 | - | outType := ty.Out(0) | 300 | + | outType := ty.Out(0) |
| 276 | - | if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize { | 301 | + | 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) | 302 | val := reflect.New(outType) | ||
| 284 | keepAlive = append(keepAlive, val) | 303 | keepAlive = append(keepAlive, val) | ||
| 285 | - | arm64_r8 = val.Pointer() | 304 | + | addInt(val.Pointer()) |
| 305 | + | } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize { | |||
| 306 | + | isAllFloats, numFields := isAllSameFloat(outType) | |||
| 307 | + | if !isAllFloats || numFields > 4 { | |||
| 308 | + | val := reflect.New(outType) | |||
| 309 | + | keepAlive = append(keepAlive, val) | |||
| 310 | + | arm64_r8 = val.Pointer() | |||
| 311 | + | } | |||
| 312 | + | } | |||
| 313 | + | } | |||
| 314 | + | for i, v := range args { | |||
| 315 | + | if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok { | |||
| 316 | + | if i != len(args)-1 { | |||
| 317 | + | panic("purego: can only expand last parameter") | |||
| 318 | + | } | |||
| 319 | + | for _, x := range variadic { | |||
| 320 | + | keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |||
| 321 | + | } | |||
| 322 | + | continue | |||
| 323 | + | } | |||
| 324 | + | // Check if we need to start Darwin ARM64 C-style stack packing | |||
| 325 | + | if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) { | |||
| 326 | + | // Collect and separate remaining args into register vs stack | |||
| 327 | + | stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats, | |||
| 328 | + | keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |||
| 329 | + | keepAlive = newKeepAlive | |||
| 330 | + | ||||
| 331 | + | // Bundle stack arguments with C-style packing | |||
| 332 | + | bundleStackArgs(stackArgs, addStack) | |||
| 333 | + | break | |||
| 286 | } | 334 | } | ||
| 335 | + | keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |||
| 287 | } | 336 | } | ||
| 337 | + | return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, arm64_r8, args) | |||
| 288 | } | 338 | } | ||
| 339 | + | ||||
| 340 | + | // Fast path: no keepAlive needed (pointer/integer/bool/float args only). | |||
| 341 | + | // Skip keepAlive slice allocation and defer overhead. | |||
| 342 | + | defer runtime.KeepAlive(args) | |||
| 343 | + | ||||
| 289 | for i, v := range args { | 344 | for i, v := range args { | ||
| 290 | if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok { | 345 | if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok { | ||
| 291 | if i != len(args)-1 { | 346 | if i != len(args)-1 { | ||
| 292 | panic("purego: can only expand last parameter") | 347 | panic("purego: can only expand last parameter") | ||
| 293 | } | 348 | } | ||
| 294 | for _, x := range variadic { | 349 | for _, x := range variadic { | ||
| 295 | - | keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | 350 | + | addValue(reflect.ValueOf(x), nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) |
| 296 | } | 351 | } | ||
| 297 | continue | 352 | continue | ||
| 298 | } | 353 | } | ||
| 299 | // Check if we need to start Darwin ARM64 C-style stack packing | 354 | // Check if we need to start Darwin ARM64 C-style stack packing | ||
| 300 | if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) { | 355 | if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) { | ||
| 301 | - | // Collect and separate remaining args into register vs stack | 356 | + | stackArgs, _ := collectStackArgs(args, i, numInts, numFloats, |
| 302 | - | stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats, | 357 | + | nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) |
| 303 | - | keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | |||
| 304 | - | keepAlive = newKeepAlive | |||
| 305 | - | ||||
| 306 | - | // Bundle stack arguments with C-style packing | |||
| 307 | bundleStackArgs(stackArgs, addStack) | 358 | bundleStackArgs(stackArgs, addStack) | ||
| 308 | break | 359 | break | ||
| 309 | } | 360 | } | ||
| 310 | - | keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) | 361 | + | addValue(v, nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) |
| 311 | } | 362 | } | ||
| 312 | 363 | ||||
| 313 | - | syscall := thePool.Get().(*syscall15Args) | 364 | + | return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, 0, args) |
| 314 | - | defer thePool.Put(syscall) | 365 | + | }) |
| 366 | + | fn.Set(v) | |||
| 367 | + | } | |||
| 368 | + | ||||
| 369 | + | // callAndReturn performs the actual C function call and constructs the return value. | |||
| 370 | + | // It is shared between the fast path (no keepAlive) and slow path (with keepAlive). | |||
| 371 | + | func callAndReturn(ty reflect.Type, is32bit bool, cfn uintptr, sysargs *[maxArgs]uintptr, floats *[maxArgs]uintptr, arm64_r8 uintptr, args []reflect.Value) []reflect.Value { | |||
| 372 | + | syscall := thePool.Get().(*syscall15Args) | |||
| 373 | + | defer thePool.Put(syscall) | |||
| 315 | 374 | ||||
| 316 | - | if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" { | 375 | + | if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" { |
| 317 | - | syscall.Set(cfn, sysargs[:], floats[:], 0) | 376 | + | syscall.Set(cfn, sysargs[:], floats[:], 0) |
| 318 | - | runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall)) | 377 | + | runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall)) |
| 319 | - | } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { | 378 | + | } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { |
| 320 | - | // Use the normal arm64 calling convention even on Windows | 379 | + | // Use the normal arm64 calling convention even on Windows |
| 321 | - | syscall.Set(cfn, sysargs[:], floats[:], arm64_r8) | 380 | + | syscall.Set(cfn, sysargs[:], floats[:], arm64_r8) |
| 322 | - | runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall)) | 381 | + | runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall)) |
| 323 | - | } else { | 382 | + | } else { |
| 324 | - | *syscall = syscall15Args{} | 383 | + | *syscall = syscall15Args{} |
| 325 | - | // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats | 384 | + | // 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], | 385 | + | 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], | 386 | + | sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11], |
| 328 | - | sysargs[12], sysargs[13], sysargs[14]) | 387 | + | 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 | 388 | + | syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support |
| 389 | + | } | |||
| 390 | + | if ty.NumOut() == 0 { | |||
| 391 | + | return nil | |||
| 392 | + | } | |||
| 393 | + | outType := ty.Out(0) | |||
| 394 | + | // Task 5: For simple scalar return types, construct the reflect.Value directly | |||
| 395 | + | // instead of allocating via reflect.New(outType).Elem(). | |||
| 396 | + | // reflect.ValueOf for scalar types avoids the heap allocation that reflect.New incurs. | |||
| 397 | + | var v reflect.Value | |||
| 398 | + | switch outType.Kind() { | |||
| 399 | + | case reflect.Uintptr: | |||
| 400 | + | v = reflect.ValueOf(uintptr(syscall.a1)) | |||
| 401 | + | case reflect.Uint: | |||
| 402 | + | v = reflect.ValueOf(uint(syscall.a1)) | |||
| 403 | + | case reflect.Uint8: | |||
| 404 | + | v = reflect.ValueOf(uint8(syscall.a1)) | |||
| 405 | + | case reflect.Uint16: | |||
| 406 | + | v = reflect.ValueOf(uint16(syscall.a1)) | |||
| 407 | + | case reflect.Uint32: | |||
| 408 | + | v = reflect.ValueOf(uint32(syscall.a1)) | |||
| 409 | + | case reflect.Uint64: | |||
| 410 | + | v = reflect.ValueOf(uint64(syscall.a1)) | |||
| 411 | + | case reflect.Int: | |||
| 412 | + | v = reflect.ValueOf(int(syscall.a1)) | |||
| 413 | + | case reflect.Int8: | |||
| 414 | + | v = reflect.ValueOf(int8(syscall.a1)) | |||
| 415 | + | case reflect.Int16: | |||
| 416 | + | v = reflect.ValueOf(int16(syscall.a1)) | |||
| 417 | + | case reflect.Int32: | |||
| 418 | + | v = reflect.ValueOf(int32(syscall.a1)) | |||
| 419 | + | case reflect.Int64: | |||
| 420 | + | v = reflect.ValueOf(int64(syscall.a1)) | |||
| 421 | + | case reflect.Bool: | |||
| 422 | + | v = reflect.ValueOf(byte(syscall.a1) != 0) | |||
| 423 | + | case reflect.UnsafePointer: | |||
| 424 | + | // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer | |||
| 425 | + | v = reflect.New(outType).Elem() | |||
| 426 | + | v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))) | |||
| 427 | + | case reflect.Pointer: | |||
| 428 | + | v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem() | |||
| 429 | + | case reflect.Func: | |||
| 430 | + | // wrap this C function in a nicely typed Go function | |||
| 431 | + | v = reflect.New(outType) | |||
| 432 | + | RegisterFunc(v.Interface(), syscall.a1) | |||
| 433 | + | case reflect.String: | |||
| 434 | + | v = reflect.ValueOf(strings.GoString(syscall.a1)) | |||
| 435 | + | case reflect.Float32: | |||
| 436 | + | // NOTE: syscall.r2 is only the floating return value on 64bit platforms. | |||
| 437 | + | // On 32bit platforms syscall.r2 is the upper part of a 64bit return. | |||
| 438 | + | // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert. | |||
| 439 | + | // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64. | |||
| 440 | + | // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register. | |||
| 441 | + | v = reflect.New(outType).Elem() | |||
| 442 | + | switch runtime.GOARCH { | |||
| 443 | + | case "386": | |||
| 444 | + | v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32))) | |||
| 445 | + | case "ppc64le": | |||
| 446 | + | v.SetFloat(math.Float64frombits(uint64(syscall.f1))) | |||
| 447 | + | case "s390x": | |||
| 448 | + | // S390X is big-endian: float32 in upper 32 bits of 64-bit register | |||
| 449 | + | v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32)))) | |||
| 450 | + | default: | |||
| 451 | + | v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1)))) | |||
| 330 | } | 452 | } | ||
| 331 | - | if ty.NumOut() == 0 { | 453 | + | case reflect.Float64: |
| 332 | - | return nil | 454 | + | // NOTE: syscall.r2 is only the floating return value on 64bit platforms. |
| 455 | + | // On 32bit platforms syscall.r2 is the upper part of a 64bit return. | |||
| 456 | + | v = reflect.New(outType).Elem() | |||
| 457 | + | if is32bit { | |||
| 458 | + | v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32))) | |||
| 459 | + | } else { | |||
| 460 | + | v.SetFloat(math.Float64frombits(uint64(syscall.f1))) | |||
| 333 | } | 461 | } | ||
| 334 | - | outType := ty.Out(0) | 462 | + | case reflect.Struct: |
| 335 | - | v := reflect.New(outType).Elem() | 463 | + | v = getStruct(outType, *syscall) |
| 336 | - | switch outType.Kind() { | 464 | + | case reflect.Slice: |
| 337 | - | case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | 465 | + | if outType.Elem().Kind() == reflect.String { |
| 338 | - | v.SetUint(uint64(syscall.a1)) | 466 | + | // C returns char** (NULL-terminated array of strings) |
| 339 | - | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | 467 | + | if syscall.a1 == 0 { |
| 340 | - | v.SetInt(int64(syscall.a1)) | 468 | + | v = reflect.Zero(outType) |
| 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.Pointer: | |||
| 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 { | 469 | } else { | ||
| 377 | - | v.SetFloat(math.Float64frombits(uint64(syscall.f1))) | 470 | + | var result []string |
| 471 | + | ptr := syscall.a1 | |||
| 472 | + | for { | |||
| 473 | + | strPtr := *(*uintptr)(unsafe.Pointer(ptr)) | |||
| 474 | + | if strPtr == 0 { | |||
| 475 | + | break | |||
| 476 | + | } | |||
| 477 | + | result = append(result, strings.GoString(strPtr)) | |||
| 478 | + | ptr += unsafe.Sizeof(uintptr(0)) | |||
| 479 | + | } | |||
| 480 | + | v = reflect.ValueOf(result) | |||
| 378 | } | 481 | } | ||
| 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 { | 482 | } else { | ||
| 389 | - | return []reflect.Value{v} | 483 | + | panic("purego: unsupported return slice element kind: " + outType.Elem().Kind().String()) |
| 390 | } | 484 | } | ||
| 391 | - | }) | 485 | + | default: |
| 392 | - | fn.Set(v) | 486 | + | panic("purego: unsupported return kind: " + outType.Kind().String()) |
| 487 | + | } | |||
| 488 | + | if len(args) > 0 { | |||
| 489 | + | // reuse args slice instead of allocating one when possible | |||
| 490 | + | args[0] = v | |||
| 491 | + | return args[:1] | |||
| 492 | + | } else { | |||
| 493 | + | return []reflect.Value{v} | |||
| 494 | + | } | |||
| 393 | } | 495 | } | ||
| 394 | 496 | ||||
| 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 { | 497 | 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 { | ||
| @@ -403,9 +505,27 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat | |||||
| 403 | addInt(uintptr(v.Uint())) | 505 | addInt(uintptr(v.Uint())) | ||
| 404 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | 506 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | ||
| 405 | addInt(uintptr(v.Int())) | 507 | addInt(uintptr(v.Int())) | ||
| 406 | - | case reflect.Pointer, reflect.UnsafePointer, reflect.Slice: | 508 | + | case reflect.Pointer, reflect.UnsafePointer: |
| 407 | // There is no need to keepAlive this pointer separately because it is kept alive in the args variable | 509 | // There is no need to keepAlive this pointer separately because it is kept alive in the args variable | ||
| 408 | addInt(v.Pointer()) | 510 | addInt(v.Pointer()) | ||
| 511 | + | case reflect.Slice: | |||
| 512 | + | if v.Type().Elem().Kind() == reflect.String { | |||
| 513 | + | ss := v.Interface().([]string) | |||
| 514 | + | if ss == nil { | |||
| 515 | + | addInt(0) | |||
| 516 | + | } else { | |||
| 517 | + | res := make([]*byte, len(ss)+1) | |||
| 518 | + | for i, s := range ss { | |||
| 519 | + | res[i] = strings.CString(s) | |||
| 520 | + | } | |||
| 521 | + | // NULL-terminated for C | |||
| 522 | + | res[len(ss)] = nil | |||
| 523 | + | keepAlive = append(keepAlive, res) | |||
| 524 | + | addInt(uintptr(unsafe.Pointer(&res[0]))) | |||
| 525 | + | } | |||
| 526 | + | } else { | |||
| 527 | + | addInt(v.Pointer()) | |||
| 528 | + | } | |||
| 409 | case reflect.Func: | 529 | case reflect.Func: | ||
| 410 | addInt(NewCallback(v.Interface())) | 530 | addInt(NewCallback(v.Interface())) | ||
| 411 | case reflect.Bool: | 531 | case reflect.Bool: | ||
| @@ -429,6 +549,17 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat | |||||
| 429 | } else { | 549 | } else { | ||
| 430 | addFloat(uintptr(math.Float64bits(v.Float()))) | 550 | addFloat(uintptr(math.Float64bits(v.Float()))) | ||
| 431 | } | 551 | } | ||
| 552 | + | case reflect.Array: | |||
| 553 | + | // Task 7: Support fixed-size array arguments by passing a pointer to the array data. | |||
| 554 | + | if v.CanAddr() { | |||
| 555 | + | addInt(v.Addr().Pointer()) | |||
| 556 | + | } else { | |||
| 557 | + | // Array is not addressable (e.g. passed by value); make an addressable copy. | |||
| 558 | + | tmp := reflect.New(v.Type()).Elem() | |||
| 559 | + | tmp.Set(v) | |||
| 560 | + | keepAlive = append(keepAlive, tmp) | |||
| 561 | + | addInt(tmp.Addr().Pointer()) | |||
| 562 | + | } | |||
| 432 | case reflect.Struct: | 563 | case reflect.Struct: | ||
| 433 | keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive) | 564 | keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive) | ||
| 434 | default: | 565 | default: | ||
A hostlayout_go123.go +12 -0
| @@ -0,0 +1,12 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2026 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | //go:build go1.23 | |
| 5 | + | ||
| 6 | + | package purego | |
| 7 | + | ||
| 8 | + | import "structs" | |
| 9 | + | ||
| 10 | + | // hostLayout is structs.HostLayout on Go 1.23+, ensuring the compiler | |
| 11 | + | // uses host (C-compatible) memory layout for structs that embed it. | |
| 12 | + | type hostLayout = structs.HostLayout | |
| @@ -0,0 +1,12 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2026 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | //go:build go1.23 | |||
| 5 | + | ||||
| 6 | + | package purego | |||
| 7 | + | ||||
| 8 | + | import "structs" | |||
| 9 | + | ||||
| 10 | + | // hostLayout is structs.HostLayout on Go 1.23+, ensuring the compiler | |||
| 11 | + | // uses host (C-compatible) memory layout for structs that embed it. | |||
| 12 | + | type hostLayout = structs.HostLayout | |||
A hostlayout_nogo123.go +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | + | // SPDX-FileCopyrightText: 2026 The Ebitengine Authors | |
| 3 | + | ||
| 4 | + | //go:build !go1.23 | |
| 5 | + | ||
| 6 | + | package purego | |
| 7 | + | ||
| 8 | + | // hostLayout is a zero-size placeholder for Go versions before 1.23 | |
| 9 | + | // that lack the structs package. On Go 1.23+ this is replaced by | |
| 10 | + | // structs.HostLayout which tells the compiler to use host memory layout. | |
| 11 | + | type hostLayout struct{} | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | // SPDX-License-Identifier: Apache-2.0 | |||
| 2 | + | // SPDX-FileCopyrightText: 2026 The Ebitengine Authors | |||
| 3 | + | ||||
| 4 | + | //go:build !go1.23 | |||
| 5 | + | ||||
| 6 | + | package purego | |||
| 7 | + | ||||
| 8 | + | // hostLayout is a zero-size placeholder for Go versions before 1.23 | |||
| 9 | + | // that lack the structs package. On Go 1.23+ this is replaced by | |||
| 10 | + | // structs.HostLayout which tells the compiler to use host memory layout. | |||
| 11 | + | type hostLayout struct{} | |||
M syscall.go +21 -0
| @@ -17,6 +17,7 @@ const ( | |||
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | 19 | type syscall15Args struct { | |
| 20 | + | _ hostLayout | |
| 20 | 21 | fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr | |
| 21 | 22 | f1, f2, f3, f4, f5, f6, f7, f8 uintptr | |
| 22 | 23 | arm64_r8 uintptr | |
| @@ -81,3 +82,23 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { | |||
| 81 | 82 | copy(tmp[:], args) | |
| 82 | 83 | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | |
| 83 | 84 | } | |
| 85 | + | ||
| 86 | + | // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation. | |
| 87 | + | // It is designed for method-like calls where the first argument is always a receiver pointer. | |
| 88 | + | // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...). | |
| 89 | + | // | |
| 90 | + | //go:uintptrescapes | |
| 91 | + | func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) { | |
| 92 | + | if fn == 0 { | |
| 93 | + | panic("purego: fn is nil") | |
| 94 | + | } | |
| 95 | + | if len(args) > maxArgs-1 { | |
| 96 | + | panic("purego: too many arguments to SyscallSelf") | |
| 97 | + | } | |
| 98 | + | // Use a stack-allocated array to prepend self without heap allocation. | |
| 99 | + | // self goes in tmp[0], remaining args follow. | |
| 100 | + | var tmp [maxArgs]uintptr | |
| 101 | + | tmp[0] = self | |
| 102 | + | copy(tmp[1:], args) | |
| 103 | + | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | |
| 104 | + | } | |
| @@ -17,6 +17,7 @@ const ( | |||||
| 17 | ) | 17 | ) | ||
| 18 | 18 | ||||
| 19 | type syscall15Args struct { | 19 | type syscall15Args struct { | ||
| 20 | + | _ hostLayout | |||
| 20 | fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr | 21 | fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr | ||
| 21 | f1, f2, f3, f4, f5, f6, f7, f8 uintptr | 22 | f1, f2, f3, f4, f5, f6, f7, f8 uintptr | ||
| 22 | arm64_r8 uintptr | 23 | arm64_r8 uintptr | ||
| @@ -81,3 +82,23 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { | |||||
| 81 | copy(tmp[:], args) | 82 | copy(tmp[:], args) | ||
| 82 | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | 83 | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | ||
| 83 | } | 84 | } | ||
| 85 | + | ||||
| 86 | + | // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation. | |||
| 87 | + | // It is designed for method-like calls where the first argument is always a receiver pointer. | |||
| 88 | + | // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...). | |||
| 89 | + | // | |||
| 90 | + | //go:uintptrescapes | |||
| 91 | + | func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) { | |||
| 92 | + | if fn == 0 { | |||
| 93 | + | panic("purego: fn is nil") | |||
| 94 | + | } | |||
| 95 | + | if len(args) > maxArgs-1 { | |||
| 96 | + | panic("purego: too many arguments to SyscallSelf") | |||
| 97 | + | } | |||
| 98 | + | // Use a stack-allocated array to prepend self without heap allocation. | |||
| 99 | + | // self goes in tmp[0], remaining args follow. | |||
| 100 | + | var tmp [maxArgs]uintptr | |||
| 101 | + | tmp[0] = self | |||
| 102 | + | copy(tmp[1:], args) | |||
| 103 | + | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | |||
| 104 | + | } | |||
M syscall_32bit.go +21 -0
| @@ -17,6 +17,7 @@ const ( | |||
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | 19 | type syscall15Args struct { | |
| 20 | + | _ hostLayout | |
| 20 | 21 | fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr | |
| 21 | 22 | a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr | |
| 22 | 23 | f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr | |
| @@ -107,3 +108,23 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { | |||
| 107 | 108 | copy(tmp[:], args) | |
| 108 | 109 | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | |
| 109 | 110 | } | |
| 111 | + | ||
| 112 | + | // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation. | |
| 113 | + | // It is designed for method-like calls where the first argument is always a receiver pointer. | |
| 114 | + | // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...). | |
| 115 | + | // | |
| 116 | + | //go:uintptrescapes | |
| 117 | + | func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) { | |
| 118 | + | if fn == 0 { | |
| 119 | + | panic("purego: fn is nil") | |
| 120 | + | } | |
| 121 | + | if len(args) > maxArgs-1 { | |
| 122 | + | panic("purego: too many arguments to SyscallSelf") | |
| 123 | + | } | |
| 124 | + | // Use a stack-allocated array to prepend self without heap allocation. | |
| 125 | + | // self goes in tmp[0], remaining args follow. | |
| 126 | + | var tmp [maxArgs]uintptr | |
| 127 | + | tmp[0] = self | |
| 128 | + | copy(tmp[1:], args) | |
| 129 | + | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | |
| 130 | + | } | |
| @@ -17,6 +17,7 @@ const ( | |||||
| 17 | ) | 17 | ) | ||
| 18 | 18 | ||||
| 19 | type syscall15Args struct { | 19 | type syscall15Args struct { | ||
| 20 | + | _ hostLayout | |||
| 20 | fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr | 21 | fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr | ||
| 21 | a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr | 22 | a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr | ||
| 22 | f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr | 23 | f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr | ||
| @@ -107,3 +108,23 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { | |||||
| 107 | copy(tmp[:], args) | 108 | copy(tmp[:], args) | ||
| 108 | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | 109 | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | ||
| 109 | } | 110 | } | ||
| 111 | + | ||||
| 112 | + | // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation. | |||
| 113 | + | // It is designed for method-like calls where the first argument is always a receiver pointer. | |||
| 114 | + | // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...). | |||
| 115 | + | // | |||
| 116 | + | //go:uintptrescapes | |||
| 117 | + | func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) { | |||
| 118 | + | if fn == 0 { | |||
| 119 | + | panic("purego: fn is nil") | |||
| 120 | + | } | |||
| 121 | + | if len(args) > maxArgs-1 { | |||
| 122 | + | panic("purego: too many arguments to SyscallSelf") | |||
| 123 | + | } | |||
| 124 | + | // Use a stack-allocated array to prepend self without heap allocation. | |||
| 125 | + | // self goes in tmp[0], remaining args follow. | |||
| 126 | + | var tmp [maxArgs]uintptr | |||
| 127 | + | tmp[0] = self | |||
| 128 | + | copy(tmp[1:], args) | |||
| 129 | + | return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) | |||
| 130 | + | } | |||
M syscall_notstackargs.go +1 -0
| @@ -17,6 +17,7 @@ import "unsafe" | |||
| 17 | 17 | // the runtime's own callback mechanism, so this type is compiled but unused, | |
| 18 | 18 | // serving only as a stub to satisfy cross-platform compilation. | |
| 19 | 19 | type callbackArgs struct { | |
| 20 | + | _ hostLayout | |
| 20 | 21 | index uintptr | |
| 21 | 22 | // args points to the argument block. | |
| 22 | 23 | // | |
| @@ -17,6 +17,7 @@ import "unsafe" | |||||
| 17 | // the runtime's own callback mechanism, so this type is compiled but unused, | 17 | // the runtime's own callback mechanism, so this type is compiled but unused, | ||
| 18 | // serving only as a stub to satisfy cross-platform compilation. | 18 | // serving only as a stub to satisfy cross-platform compilation. | ||
| 19 | type callbackArgs struct { | 19 | type callbackArgs struct { | ||
| 20 | + | _ hostLayout | |||
| 20 | index uintptr | 21 | index uintptr | ||
| 21 | // args points to the argument block. | 22 | // args points to the argument block. | ||
| 22 | // | 23 | // | ||
M syscall_stackargs.go +1 -0
| @@ -17,6 +17,7 @@ import "unsafe" | |||
| 17 | 17 | // the runtime's own callback mechanism, so this type is compiled but unused, | |
| 18 | 18 | // serving only as a stub to satisfy cross-platform compilation. | |
| 19 | 19 | type callbackArgs struct { | |
| 20 | + | _ hostLayout | |
| 20 | 21 | index uintptr | |
| 21 | 22 | // args points to the argument block. | |
| 22 | 23 | // | |
| @@ -17,6 +17,7 @@ import "unsafe" | |||||
| 17 | // the runtime's own callback mechanism, so this type is compiled but unused, | 17 | // the runtime's own callback mechanism, so this type is compiled but unused, | ||
| 18 | // serving only as a stub to satisfy cross-platform compilation. | 18 | // serving only as a stub to satisfy cross-platform compilation. | ||
| 19 | type callbackArgs struct { | 19 | type callbackArgs struct { | ||
| 20 | + | _ hostLayout | |||
| 20 | index uintptr | 21 | index uintptr | ||
| 21 | // args points to the argument block. | 22 | // args points to the argument block. | ||
| 22 | // | 23 | // | ||
M syscall_test.go +77 -0
| @@ -4,6 +4,7 @@ | |||
| 4 | 4 | package purego_test | |
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | + | "fmt" | |
| 7 | 8 | "os" | |
| 8 | 9 | "runtime" | |
| 9 | 10 | "testing" | |
| @@ -54,3 +55,79 @@ func TestErrno(t *testing.T) { | |||
| 54 | 55 | t.Errorf("strerror returned %q, wanted \"%s\"", got, expected) | |
| 55 | 56 | } | |
| 56 | 57 | } | |
| 58 | + | ||
| 59 | + | func TestSyscallSelf(t *testing.T) { | |
| 60 | + | // Test that SyscallSelf(fn, self, args...) produces the same result as | |
| 61 | + | // SyscallN(fn, self, args...) for various argument counts. | |
| 62 | + | ||
| 63 | + | // sum2 takes (self, a1) and returns self + a1 | |
| 64 | + | sum2 := purego.NewCallback(func(self, a1 uintptr) uintptr { | |
| 65 | + | return self + a1 | |
| 66 | + | }) | |
| 67 | + | // sum4 takes (self, a1, a2, a3) and returns self + a1 + a2 + a3 | |
| 68 | + | sum4 := purego.NewCallback(func(self, a1, a2, a3 uintptr) uintptr { | |
| 69 | + | return self + a1 + a2 + a3 | |
| 70 | + | }) | |
| 71 | + | ||
| 72 | + | tests := []struct { | |
| 73 | + | name string | |
| 74 | + | fn uintptr | |
| 75 | + | self uintptr | |
| 76 | + | args []uintptr | |
| 77 | + | want uintptr | |
| 78 | + | }{ | |
| 79 | + | {"no extra args", sum2, 10, []uintptr{20}, 30}, | |
| 80 | + | {"zero self", sum2, 0, []uintptr{42}, 42}, | |
| 81 | + | {"multiple args", sum4, 1, []uintptr{2, 3, 4}, 10}, | |
| 82 | + | {"no variadic args", | |
| 83 | + | purego.NewCallback(func(self uintptr) uintptr { return self * 3 }), | |
| 84 | + | 7, nil, 21}, | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | for _, tc := range tests { | |
| 88 | + | t.Run(tc.name, func(t *testing.T) { | |
| 89 | + | r1, _, _ := purego.SyscallSelf(tc.fn, tc.self, tc.args...) | |
| 90 | + | if r1 != tc.want { | |
| 91 | + | t.Errorf("SyscallSelf returned %d, want %d", r1, tc.want) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // Verify it matches SyscallN with manually prepended self | |
| 95 | + | allArgs := make([]uintptr, 0, 1+len(tc.args)) | |
| 96 | + | allArgs = append(allArgs, tc.self) | |
| 97 | + | allArgs = append(allArgs, tc.args...) | |
| 98 | + | r1n, _, _ := purego.SyscallN(tc.fn, allArgs...) | |
| 99 | + | if r1 != r1n { | |
| 100 | + | t.Errorf("SyscallSelf result %d differs from SyscallN result %d", r1, r1n) | |
| 101 | + | } | |
| 102 | + | }) | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func TestSyscallSelfPanics(t *testing.T) { | |
| 107 | + | t.Run("nil fn", func(t *testing.T) { | |
| 108 | + | defer func() { | |
| 109 | + | if r := recover(); r == nil { | |
| 110 | + | t.Error("expected panic for nil fn") | |
| 111 | + | } | |
| 112 | + | }() | |
| 113 | + | purego.SyscallSelf(0, 1) | |
| 114 | + | }) | |
| 115 | + | ||
| 116 | + | t.Run("too many args", func(t *testing.T) { | |
| 117 | + | defer func() { | |
| 118 | + | r := recover() | |
| 119 | + | if r == nil { | |
| 120 | + | t.Error("expected panic for too many args") | |
| 121 | + | } | |
| 122 | + | msg := fmt.Sprint(r) | |
| 123 | + | if msg != "purego: too many arguments to SyscallSelf" { | |
| 124 | + | t.Errorf("unexpected panic message: %s", msg) | |
| 125 | + | } | |
| 126 | + | }() | |
| 127 | + | // Create a valid callback so we don't panic on nil fn | |
| 128 | + | fn := purego.NewCallback(func(uintptr) uintptr { return 0 }) | |
| 129 | + | // Pass more args than maxArgs-1 allows (maxArgs is 15 on 64-bit) | |
| 130 | + | args := make([]uintptr, 15) | |
| 131 | + | purego.SyscallSelf(fn, 1, args...) | |
| 132 | + | }) | |
| 133 | + | } | |
| @@ -4,6 +4,7 @@ | |||||
| 4 | package purego_test | 4 | package purego_test | ||
| 5 | 5 | ||||
| 6 | import ( | 6 | import ( | ||
| 7 | + | "fmt" | |||
| 7 | "os" | 8 | "os" | ||
| 8 | "runtime" | 9 | "runtime" | ||
| 9 | "testing" | 10 | "testing" | ||
| @@ -54,3 +55,79 @@ func TestErrno(t *testing.T) { | |||||
| 54 | t.Errorf("strerror returned %q, wanted \"%s\"", got, expected) | 55 | t.Errorf("strerror returned %q, wanted \"%s\"", got, expected) | ||
| 55 | } | 56 | } | ||
| 56 | } | 57 | } | ||
| 58 | + | ||||
| 59 | + | func TestSyscallSelf(t *testing.T) { | |||
| 60 | + | // Test that SyscallSelf(fn, self, args...) produces the same result as | |||
| 61 | + | // SyscallN(fn, self, args...) for various argument counts. | |||
| 62 | + | ||||
| 63 | + | // sum2 takes (self, a1) and returns self + a1 | |||
| 64 | + | sum2 := purego.NewCallback(func(self, a1 uintptr) uintptr { | |||
| 65 | + | return self + a1 | |||
| 66 | + | }) | |||
| 67 | + | // sum4 takes (self, a1, a2, a3) and returns self + a1 + a2 + a3 | |||
| 68 | + | sum4 := purego.NewCallback(func(self, a1, a2, a3 uintptr) uintptr { | |||
| 69 | + | return self + a1 + a2 + a3 | |||
| 70 | + | }) | |||
| 71 | + | ||||
| 72 | + | tests := []struct { | |||
| 73 | + | name string | |||
| 74 | + | fn uintptr | |||
| 75 | + | self uintptr | |||
| 76 | + | args []uintptr | |||
| 77 | + | want uintptr | |||
| 78 | + | }{ | |||
| 79 | + | {"no extra args", sum2, 10, []uintptr{20}, 30}, | |||
| 80 | + | {"zero self", sum2, 0, []uintptr{42}, 42}, | |||
| 81 | + | {"multiple args", sum4, 1, []uintptr{2, 3, 4}, 10}, | |||
| 82 | + | {"no variadic args", | |||
| 83 | + | purego.NewCallback(func(self uintptr) uintptr { return self * 3 }), | |||
| 84 | + | 7, nil, 21}, | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | for _, tc := range tests { | |||
| 88 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 89 | + | r1, _, _ := purego.SyscallSelf(tc.fn, tc.self, tc.args...) | |||
| 90 | + | if r1 != tc.want { | |||
| 91 | + | t.Errorf("SyscallSelf returned %d, want %d", r1, tc.want) | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // Verify it matches SyscallN with manually prepended self | |||
| 95 | + | allArgs := make([]uintptr, 0, 1+len(tc.args)) | |||
| 96 | + | allArgs = append(allArgs, tc.self) | |||
| 97 | + | allArgs = append(allArgs, tc.args...) | |||
| 98 | + | r1n, _, _ := purego.SyscallN(tc.fn, allArgs...) | |||
| 99 | + | if r1 != r1n { | |||
| 100 | + | t.Errorf("SyscallSelf result %d differs from SyscallN result %d", r1, r1n) | |||
| 101 | + | } | |||
| 102 | + | }) | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func TestSyscallSelfPanics(t *testing.T) { | |||
| 107 | + | t.Run("nil fn", func(t *testing.T) { | |||
| 108 | + | defer func() { | |||
| 109 | + | if r := recover(); r == nil { | |||
| 110 | + | t.Error("expected panic for nil fn") | |||
| 111 | + | } | |||
| 112 | + | }() | |||
| 113 | + | purego.SyscallSelf(0, 1) | |||
| 114 | + | }) | |||
| 115 | + | ||||
| 116 | + | t.Run("too many args", func(t *testing.T) { | |||
| 117 | + | defer func() { | |||
| 118 | + | r := recover() | |||
| 119 | + | if r == nil { | |||
| 120 | + | t.Error("expected panic for too many args") | |||
| 121 | + | } | |||
| 122 | + | msg := fmt.Sprint(r) | |||
| 123 | + | if msg != "purego: too many arguments to SyscallSelf" { | |||
| 124 | + | t.Errorf("unexpected panic message: %s", msg) | |||
| 125 | + | } | |||
| 126 | + | }() | |||
| 127 | + | // Create a valid callback so we don't panic on nil fn | |||
| 128 | + | fn := purego.NewCallback(func(uintptr) uintptr { return 0 }) | |||
| 129 | + | // Pass more args than maxArgs-1 allows (maxArgs is 15 on 64-bit) | |||
| 130 | + | args := make([]uintptr, 15) | |||
| 131 | + | purego.SyscallSelf(fn, 1, args...) | |||
| 132 | + | }) | |||
| 133 | + | } | |||
M syscall_unix.go +23 -8
| @@ -55,9 +55,10 @@ func NewCallback(fn any) uintptr { | |||
| 55 | 55 | const maxCB = 2000 | |
| 56 | 56 | ||
| 57 | 57 | var cbs struct { | |
| 58 | - | lock sync.Mutex | |
| 59 | - | numFn int // the number of functions currently in cbs.funcs | |
| 60 | - | funcs [maxCB]reflect.Value // the saved callbacks | |
| 58 | + | lock sync.RWMutex | |
| 59 | + | numFn int // the number of functions currently in cbs.funcs | |
| 60 | + | funcs [maxCB]reflect.Value // the saved callbacks | |
| 61 | + | argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback | |
| 61 | 62 | } | |
| 62 | 63 | ||
| 63 | 64 | func compileCallback(fn any) uintptr { | |
| @@ -103,9 +104,16 @@ output: | |||
| 103 | 104 | if cbs.numFn >= maxCB { | |
| 104 | 105 | panic("purego: the maximum number of callbacks has been reached") | |
| 105 | 106 | } | |
| 106 | - | cbs.funcs[cbs.numFn] = val | |
| 107 | + | index := cbs.numFn | |
| 108 | + | cbs.funcs[index] = val | |
| 109 | + | numIn := ty.NumIn() | |
| 110 | + | cbs.argPools[index] = &sync.Pool{ | |
| 111 | + | New: func() any { | |
| 112 | + | return make([]reflect.Value, numIn) | |
| 113 | + | }, | |
| 114 | + | } | |
| 107 | 115 | cbs.numFn++ | |
| 108 | - | return callbackasmAddr(cbs.numFn - 1) | |
| 116 | + | return callbackasmAddr(index) | |
| 109 | 117 | } | |
| 110 | 118 | ||
| 111 | 119 | const ptrSize = unsafe.Sizeof((*int)(nil)) | |
| @@ -127,11 +135,18 @@ var callbackWrap_call = callbackWrap | |||
| 127 | 135 | // callbackWrap is called by assembly code which determines which Go function to call. | |
| 128 | 136 | // This function takes the arguments and passes them to the Go function and returns the result. | |
| 129 | 137 | func callbackWrap(a *callbackArgs) { | |
| 130 | - | cbs.lock.Lock() | |
| 138 | + | cbs.lock.RLock() | |
| 131 | 139 | fn := cbs.funcs[a.index] | |
| 132 | - | cbs.lock.Unlock() | |
| 140 | + | pool := cbs.argPools[a.index] | |
| 141 | + | cbs.lock.RUnlock() | |
| 133 | 142 | fnType := fn.Type() | |
| 134 | - | args := make([]reflect.Value, fnType.NumIn()) | |
| 143 | + | args := pool.Get().([]reflect.Value) | |
| 144 | + | defer func() { | |
| 145 | + | for i := range args { | |
| 146 | + | args[i] = reflect.Value{} | |
| 147 | + | } | |
| 148 | + | pool.Put(args) | |
| 149 | + | }() | |
| 135 | 150 | frame := (*[callbackMaxFrame]uintptr)(a.args) | |
| 136 | 151 | // stackFrame points to stack-passed arguments. On most architectures this is | |
| 137 | 152 | // contiguous with frame (after register args), but on ppc64le it's separate. | |
| @@ -55,9 +55,10 @@ func NewCallback(fn any) uintptr { | |||||
| 55 | const maxCB = 2000 | 55 | const maxCB = 2000 | ||
| 56 | 56 | ||||
| 57 | var cbs struct { | 57 | var cbs struct { | ||
| 58 | - | lock sync.Mutex | 58 | + | lock sync.RWMutex |
| 59 | - | numFn int // the number of functions currently in cbs.funcs | 59 | + | numFn int // the number of functions currently in cbs.funcs |
| 60 | - | funcs [maxCB]reflect.Value // the saved callbacks | 60 | + | funcs [maxCB]reflect.Value // the saved callbacks |
| 61 | + | argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback | |||
| 61 | } | 62 | } | ||
| 62 | 63 | ||||
| 63 | func compileCallback(fn any) uintptr { | 64 | func compileCallback(fn any) uintptr { | ||
| @@ -103,9 +104,16 @@ output: | |||||
| 103 | if cbs.numFn >= maxCB { | 104 | if cbs.numFn >= maxCB { | ||
| 104 | panic("purego: the maximum number of callbacks has been reached") | 105 | panic("purego: the maximum number of callbacks has been reached") | ||
| 105 | } | 106 | } | ||
| 106 | - | cbs.funcs[cbs.numFn] = val | 107 | + | index := cbs.numFn |
| 108 | + | cbs.funcs[index] = val | |||
| 109 | + | numIn := ty.NumIn() | |||
| 110 | + | cbs.argPools[index] = &sync.Pool{ | |||
| 111 | + | New: func() any { | |||
| 112 | + | return make([]reflect.Value, numIn) | |||
| 113 | + | }, | |||
| 114 | + | } | |||
| 107 | cbs.numFn++ | 115 | cbs.numFn++ | ||
| 108 | - | return callbackasmAddr(cbs.numFn - 1) | 116 | + | return callbackasmAddr(index) |
| 109 | } | 117 | } | ||
| 110 | 118 | ||||
| 111 | const ptrSize = unsafe.Sizeof((*int)(nil)) | 119 | const ptrSize = unsafe.Sizeof((*int)(nil)) | ||
| @@ -127,11 +135,18 @@ var callbackWrap_call = callbackWrap | |||||
| 127 | // callbackWrap is called by assembly code which determines which Go function to call. | 135 | // callbackWrap is called by assembly code which determines which Go function to call. | ||
| 128 | // This function takes the arguments and passes them to the Go function and returns the result. | 136 | // This function takes the arguments and passes them to the Go function and returns the result. | ||
| 129 | func callbackWrap(a *callbackArgs) { | 137 | func callbackWrap(a *callbackArgs) { | ||
| 130 | - | cbs.lock.Lock() | 138 | + | cbs.lock.RLock() |
| 131 | fn := cbs.funcs[a.index] | 139 | fn := cbs.funcs[a.index] | ||
| 132 | - | cbs.lock.Unlock() | 140 | + | pool := cbs.argPools[a.index] |
| 141 | + | cbs.lock.RUnlock() | |||
| 133 | fnType := fn.Type() | 142 | fnType := fn.Type() | ||
| 134 | - | args := make([]reflect.Value, fnType.NumIn()) | 143 | + | args := pool.Get().([]reflect.Value) |
| 144 | + | defer func() { | |||
| 145 | + | for i := range args { | |||
| 146 | + | args[i] = reflect.Value{} | |||
| 147 | + | } | |||
| 148 | + | pool.Put(args) | |||
| 149 | + | }() | |||
| 135 | frame := (*[callbackMaxFrame]uintptr)(a.args) | 150 | frame := (*[callbackMaxFrame]uintptr)(a.args) | ||
| 136 | // stackFrame points to stack-passed arguments. On most architectures this is | 151 | // stackFrame points to stack-passed arguments. On most architectures this is | ||
| 137 | // contiguous with frame (after register args), but on ppc64le it's separate. | 152 | // contiguous with frame (after register args), but on ppc64le it's separate. | ||