feat(callback): add reflection-free NewCallbackInts fast path
- commit
- f8448787cf22349a771da82d3c35dc6dac1a9444
- author
- Brice · · 2026-09-27 18:01 UTC
- committer
- brice · · 2026-09-27 18:45 UTC
- parent
- 06b205c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +90 -9
M syscall_unix.go +75 -9
| @@ -56,6 +56,40 @@ func NewCallback(fn any) uintptr { | |||
| 56 | 56 | return compileCallback(fn) | |
| 57 | 57 | } | |
| 58 | 58 | ||
| 59 | + | // CallbackArgs is the integer-class argument block of one C call into a | |
| 60 | + | // NewCallbackInts callback. It is only valid during the callback. | |
| 61 | + | type CallbackArgs struct { | |
| 62 | + | frame unsafe.Pointer | |
| 63 | + | intFrame unsafe.Pointer | |
| 64 | + | stackFrame unsafe.Pointer | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // Int returns the i-th integer-class argument (0-based), following the | |
| 68 | + | // platform C ABI: integer registers first, then stack slots. | |
| 69 | + | func (a *CallbackArgs) Int(i int) uintptr { | |
| 70 | + | if i < numOfIntegerRegisters() { | |
| 71 | + | if a.intFrame != nil { | |
| 72 | + | return (*[callbackMaxFrame]uintptr)(a.intFrame)[i] | |
| 73 | + | } | |
| 74 | + | return (*[callbackMaxFrame]uintptr)(a.frame)[numOfFloatRegisters()+i] | |
| 75 | + | } | |
| 76 | + | k := i - numOfIntegerRegisters() | |
| 77 | + | if a.stackFrame != nil { | |
| 78 | + | return (*[callbackMaxFrame]uintptr)(a.stackFrame)[k] | |
| 79 | + | } | |
| 80 | + | return (*[callbackMaxFrame]uintptr)(a.frame)[numOfFloatRegisters()+numOfIntegerRegisters()+k] | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // NewCallbackInts returns a C function pointer that calls fn with the raw | |
| 84 | + | // integer-class arguments of the C call and returns its result in the C | |
| 85 | + | // integer return register. Only integer-class C parameters (integers, | |
| 86 | + | // pointers) are supported; float arguments are not visible. The call path | |
| 87 | + | // does no reflection and no allocation. Like NewCallback, the callback is | |
| 88 | + | // never freed and counts against the callback limit. | |
| 89 | + | func NewCallbackInts(fn func(a *CallbackArgs) uintptr) uintptr { | |
| 90 | + | return compileFastCallback(fn) | |
| 91 | + | } | |
| 92 | + | ||
| 59 | 93 | // NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention. | |
| 60 | 94 | // Calling this function multiple times with the same function pointer returns the original callback address. | |
| 61 | 95 | func NewCallbackFnPtr(fnPtr any) uintptr { | |
| @@ -101,6 +135,7 @@ func UnrefCallback(cb uintptr) error { | |||
| 101 | 135 | delete(cbs.knownIdx, cb) | |
| 102 | 136 | cbs.holes[idx] = struct{}{} | |
| 103 | 137 | cbs.funcs[idx] = reflect.Value{} | |
| 138 | + | cbs.fast[idx] = nil | |
| 104 | 139 | recordCallbackLedger("release", idx, cb, key, val, false, "") | |
| 105 | 140 | return nil | |
| 106 | 141 | } | |
| @@ -143,13 +178,14 @@ const maxCB = 2000 | |||
| 143 | 178 | ||
| 144 | 179 | var cbs = struct { | |
| 145 | 180 | lock sync.RWMutex | |
| 146 | - | numFn int // the highest allocated callback index + 1 | |
| 147 | - | holes map[int]struct{} // reusable callback slots | |
| 148 | - | funcs [maxCB]reflect.Value // the saved callbacks | |
| 149 | - | knownIdx map[uintptr]int // callback address -> slot index | |
| 150 | - | knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address | |
| 151 | - | fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address | |
| 152 | - | ledgerSeq uint64 // monotonic callback ledger event sequence | |
| 181 | + | numFn int // the highest allocated callback index + 1 | |
| 182 | + | holes map[int]struct{} // reusable callback slots | |
| 183 | + | funcs [maxCB]reflect.Value // the saved callbacks | |
| 184 | + | fast [maxCB]func(*CallbackArgs) uintptr // integer-only callbacks | |
| 185 | + | knownIdx map[uintptr]int // callback address -> slot index | |
| 186 | + | knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address | |
| 187 | + | fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address | |
| 188 | + | ledgerSeq uint64 // monotonic callback ledger event sequence | |
| 153 | 189 | }{ | |
| 154 | 190 | holes: make(map[int]struct{}), | |
| 155 | 191 | knownIdx: make(map[uintptr]int, maxCB), | |
| @@ -389,6 +425,20 @@ output: | |||
| 389 | 425 | } | |
| 390 | 426 | cbs.lock.Lock() | |
| 391 | 427 | defer cbs.lock.Unlock() | |
| 428 | + | return allocateCallback(val, nil) | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | func compileFastCallback(fn func(*CallbackArgs) uintptr) uintptr { | |
| 432 | + | if fn == nil { | |
| 433 | + | panic("purego: function must not be nil") | |
| 434 | + | } | |
| 435 | + | cbs.lock.Lock() | |
| 436 | + | defer cbs.lock.Unlock() | |
| 437 | + | return allocateCallback(reflect.Value{}, fn) | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | // allocateCallback is called with cbs.lock held. | |
| 441 | + | func allocateCallback(val reflect.Value, fast func(*CallbackArgs) uintptr) uintptr { | |
| 392 | 442 | index := -1 | |
| 393 | 443 | reused := false | |
| 394 | 444 | for i := range cbs.holes { | |
| @@ -401,16 +451,19 @@ output: | |||
| 401 | 451 | remaining := maxCB - cbs.numFn | |
| 402 | 452 | if remaining <= 0 { | |
| 403 | 453 | recordCallbackLedger("exhausted", -1, 0, 0, val, false, "maximum callbacks reached") | |
| 404 | - | traceCallbackAllocation("exhausted", val, 0) | |
| 454 | + | if val.IsValid() { | |
| 455 | + | traceCallbackAllocation("exhausted", val, 0) | |
| 456 | + | } | |
| 405 | 457 | panic("purego: the maximum number of callbacks has been reached") | |
| 406 | 458 | } | |
| 407 | - | if remaining <= 100 || remaining%250 == 0 { | |
| 459 | + | if val.IsValid() && (remaining <= 100 || remaining%250 == 0) { | |
| 408 | 460 | traceCallbackAllocation("allocated", val, remaining) | |
| 409 | 461 | } | |
| 410 | 462 | index = cbs.numFn | |
| 411 | 463 | cbs.numFn++ | |
| 412 | 464 | } | |
| 413 | 465 | cbs.funcs[index] = val | |
| 466 | + | cbs.fast[index] = fast | |
| 414 | 467 | addr := callbackasmAddr(index) | |
| 415 | 468 | cbs.knownIdx[addr] = index | |
| 416 | 469 | if reused { | |
| @@ -437,9 +490,22 @@ var callbackasmABI0 = uintptr(unsafe.Pointer(&__callbackasm)) | |||
| 437 | 490 | // This closure is used inside sys_darwin_GOARCH.s | |
| 438 | 491 | var callbackWrap_call = callbackWrap | |
| 439 | 492 | ||
| 493 | + | // noescape is runtime.noescape; the synchronous callee must not retain p. | |
| 494 | + | //go:noescape | |
| 495 | + | //go:linkname noescape runtime.noescape | |
| 496 | + | func noescape(p unsafe.Pointer) unsafe.Pointer | |
| 497 | + | ||
| 440 | 498 | // callbackWrap is called by assembly code which determines which Go function to call. | |
| 441 | 499 | // This function takes the arguments and passes them to the Go function and returns the result. | |
| 442 | 500 | func callbackWrap(a *callbackArgs) { | |
| 501 | + | // Slots are published under the lock before the C pointer is handed out. | |
| 502 | + | // A caller must not invoke a released callback while UnrefCallback reuses | |
| 503 | + | // its slot; while callable, fast slots are never cleared. | |
| 504 | + | if f := cbs.fast[a.index]; f != nil { | |
| 505 | + | ca := CallbackArgs{frame: a.args, intFrame: a.intFrame(), stackFrame: a.stackFrame()} | |
| 506 | + | a.result[0] = f((*CallbackArgs)(noescape(unsafe.Pointer(&ca)))) | |
| 507 | + | return | |
| 508 | + | } | |
| 443 | 509 | cbs.lock.Lock() | |
| 444 | 510 | fn := cbs.funcs[a.index] | |
| 445 | 511 | cbs.lock.Unlock() | |
| @@ -56,6 +56,40 @@ func NewCallback(fn any) uintptr { | |||||
| 56 | return compileCallback(fn) | 56 | return compileCallback(fn) | ||
| 57 | } | 57 | } | ||
| 58 | 58 | ||||
| 59 | + | // CallbackArgs is the integer-class argument block of one C call into a | |||
| 60 | + | // NewCallbackInts callback. It is only valid during the callback. | |||
| 61 | + | type CallbackArgs struct { | |||
| 62 | + | frame unsafe.Pointer | |||
| 63 | + | intFrame unsafe.Pointer | |||
| 64 | + | stackFrame unsafe.Pointer | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // Int returns the i-th integer-class argument (0-based), following the | |||
| 68 | + | // platform C ABI: integer registers first, then stack slots. | |||
| 69 | + | func (a *CallbackArgs) Int(i int) uintptr { | |||
| 70 | + | if i < numOfIntegerRegisters() { | |||
| 71 | + | if a.intFrame != nil { | |||
| 72 | + | return (*[callbackMaxFrame]uintptr)(a.intFrame)[i] | |||
| 73 | + | } | |||
| 74 | + | return (*[callbackMaxFrame]uintptr)(a.frame)[numOfFloatRegisters()+i] | |||
| 75 | + | } | |||
| 76 | + | k := i - numOfIntegerRegisters() | |||
| 77 | + | if a.stackFrame != nil { | |||
| 78 | + | return (*[callbackMaxFrame]uintptr)(a.stackFrame)[k] | |||
| 79 | + | } | |||
| 80 | + | return (*[callbackMaxFrame]uintptr)(a.frame)[numOfFloatRegisters()+numOfIntegerRegisters()+k] | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // NewCallbackInts returns a C function pointer that calls fn with the raw | |||
| 84 | + | // integer-class arguments of the C call and returns its result in the C | |||
| 85 | + | // integer return register. Only integer-class C parameters (integers, | |||
| 86 | + | // pointers) are supported; float arguments are not visible. The call path | |||
| 87 | + | // does no reflection and no allocation. Like NewCallback, the callback is | |||
| 88 | + | // never freed and counts against the callback limit. | |||
| 89 | + | func NewCallbackInts(fn func(a *CallbackArgs) uintptr) uintptr { | |||
| 90 | + | return compileFastCallback(fn) | |||
| 91 | + | } | |||
| 92 | + | ||||
| 59 | // NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention. | 93 | // NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention. | ||
| 60 | // Calling this function multiple times with the same function pointer returns the original callback address. | 94 | // Calling this function multiple times with the same function pointer returns the original callback address. | ||
| 61 | func NewCallbackFnPtr(fnPtr any) uintptr { | 95 | func NewCallbackFnPtr(fnPtr any) uintptr { | ||
| @@ -101,6 +135,7 @@ func UnrefCallback(cb uintptr) error { | |||||
| 101 | delete(cbs.knownIdx, cb) | 135 | delete(cbs.knownIdx, cb) | ||
| 102 | cbs.holes[idx] = struct{}{} | 136 | cbs.holes[idx] = struct{}{} | ||
| 103 | cbs.funcs[idx] = reflect.Value{} | 137 | cbs.funcs[idx] = reflect.Value{} | ||
| 138 | + | cbs.fast[idx] = nil | |||
| 104 | recordCallbackLedger("release", idx, cb, key, val, false, "") | 139 | recordCallbackLedger("release", idx, cb, key, val, false, "") | ||
| 105 | return nil | 140 | return nil | ||
| 106 | } | 141 | } | ||
| @@ -143,13 +178,14 @@ const maxCB = 2000 | |||||
| 143 | 178 | ||||
| 144 | var cbs = struct { | 179 | var cbs = struct { | ||
| 145 | lock sync.RWMutex | 180 | lock sync.RWMutex | ||
| 146 | - | numFn int // the highest allocated callback index + 1 | 181 | + | numFn int // the highest allocated callback index + 1 |
| 147 | - | holes map[int]struct{} // reusable callback slots | 182 | + | holes map[int]struct{} // reusable callback slots |
| 148 | - | funcs [maxCB]reflect.Value // the saved callbacks | 183 | + | funcs [maxCB]reflect.Value // the saved callbacks |
| 149 | - | knownIdx map[uintptr]int // callback address -> slot index | 184 | + | fast [maxCB]func(*CallbackArgs) uintptr // integer-only callbacks |
| 150 | - | knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address | 185 | + | knownIdx map[uintptr]int // callback address -> slot index |
| 151 | - | fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address | 186 | + | knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address |
| 152 | - | ledgerSeq uint64 // monotonic callback ledger event sequence | 187 | + | fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address |
| 188 | + | ledgerSeq uint64 // monotonic callback ledger event sequence | |||
| 153 | }{ | 189 | }{ | ||
| 154 | holes: make(map[int]struct{}), | 190 | holes: make(map[int]struct{}), | ||
| 155 | knownIdx: make(map[uintptr]int, maxCB), | 191 | knownIdx: make(map[uintptr]int, maxCB), | ||
| @@ -389,6 +425,20 @@ output: | |||||
| 389 | } | 425 | } | ||
| 390 | cbs.lock.Lock() | 426 | cbs.lock.Lock() | ||
| 391 | defer cbs.lock.Unlock() | 427 | defer cbs.lock.Unlock() | ||
| 428 | + | return allocateCallback(val, nil) | |||
| 429 | + | } | |||
| 430 | + | ||||
| 431 | + | func compileFastCallback(fn func(*CallbackArgs) uintptr) uintptr { | |||
| 432 | + | if fn == nil { | |||
| 433 | + | panic("purego: function must not be nil") | |||
| 434 | + | } | |||
| 435 | + | cbs.lock.Lock() | |||
| 436 | + | defer cbs.lock.Unlock() | |||
| 437 | + | return allocateCallback(reflect.Value{}, fn) | |||
| 438 | + | } | |||
| 439 | + | ||||
| 440 | + | // allocateCallback is called with cbs.lock held. | |||
| 441 | + | func allocateCallback(val reflect.Value, fast func(*CallbackArgs) uintptr) uintptr { | |||
| 392 | index := -1 | 442 | index := -1 | ||
| 393 | reused := false | 443 | reused := false | ||
| 394 | for i := range cbs.holes { | 444 | for i := range cbs.holes { | ||
| @@ -401,16 +451,19 @@ output: | |||||
| 401 | remaining := maxCB - cbs.numFn | 451 | remaining := maxCB - cbs.numFn | ||
| 402 | if remaining <= 0 { | 452 | if remaining <= 0 { | ||
| 403 | recordCallbackLedger("exhausted", -1, 0, 0, val, false, "maximum callbacks reached") | 453 | recordCallbackLedger("exhausted", -1, 0, 0, val, false, "maximum callbacks reached") | ||
| 404 | - | traceCallbackAllocation("exhausted", val, 0) | 454 | + | if val.IsValid() { |
| 455 | + | traceCallbackAllocation("exhausted", val, 0) | |||
| 456 | + | } | |||
| 405 | panic("purego: the maximum number of callbacks has been reached") | 457 | panic("purego: the maximum number of callbacks has been reached") | ||
| 406 | } | 458 | } | ||
| 407 | - | if remaining <= 100 || remaining%250 == 0 { | 459 | + | if val.IsValid() && (remaining <= 100 || remaining%250 == 0) { |
| 408 | traceCallbackAllocation("allocated", val, remaining) | 460 | traceCallbackAllocation("allocated", val, remaining) | ||
| 409 | } | 461 | } | ||
| 410 | index = cbs.numFn | 462 | index = cbs.numFn | ||
| 411 | cbs.numFn++ | 463 | cbs.numFn++ | ||
| 412 | } | 464 | } | ||
| 413 | cbs.funcs[index] = val | 465 | cbs.funcs[index] = val | ||
| 466 | + | cbs.fast[index] = fast | |||
| 414 | addr := callbackasmAddr(index) | 467 | addr := callbackasmAddr(index) | ||
| 415 | cbs.knownIdx[addr] = index | 468 | cbs.knownIdx[addr] = index | ||
| 416 | if reused { | 469 | if reused { | ||
| @@ -437,9 +490,22 @@ var callbackasmABI0 = uintptr(unsafe.Pointer(&__callbackasm)) | |||||
| 437 | // This closure is used inside sys_darwin_GOARCH.s | 490 | // This closure is used inside sys_darwin_GOARCH.s | ||
| 438 | var callbackWrap_call = callbackWrap | 491 | var callbackWrap_call = callbackWrap | ||
| 439 | 492 | ||||
| 493 | + | // noescape is runtime.noescape; the synchronous callee must not retain p. | |||
| 494 | + | //go:noescape | |||
| 495 | + | //go:linkname noescape runtime.noescape | |||
| 496 | + | func noescape(p unsafe.Pointer) unsafe.Pointer | |||
| 497 | + | ||||
| 440 | // callbackWrap is called by assembly code which determines which Go function to call. | 498 | // callbackWrap is called by assembly code which determines which Go function to call. | ||
| 441 | // This function takes the arguments and passes them to the Go function and returns the result. | 499 | // This function takes the arguments and passes them to the Go function and returns the result. | ||
| 442 | func callbackWrap(a *callbackArgs) { | 500 | func callbackWrap(a *callbackArgs) { | ||
| 501 | + | // Slots are published under the lock before the C pointer is handed out. | |||
| 502 | + | // A caller must not invoke a released callback while UnrefCallback reuses | |||
| 503 | + | // its slot; while callable, fast slots are never cleared. | |||
| 504 | + | if f := cbs.fast[a.index]; f != nil { | |||
| 505 | + | ca := CallbackArgs{frame: a.args, intFrame: a.intFrame(), stackFrame: a.stackFrame()} | |||
| 506 | + | a.result[0] = f((*CallbackArgs)(noescape(unsafe.Pointer(&ca)))) | |||
| 507 | + | return | |||
| 508 | + | } | |||
| 443 | cbs.lock.Lock() | 509 | cbs.lock.Lock() | ||
| 444 | fn := cbs.funcs[a.index] | 510 | fn := cbs.funcs[a.index] | ||
| 445 | cbs.lock.Unlock() | 511 | cbs.lock.Unlock() | ||
M syscall_windows.go +15 -0
| @@ -52,6 +52,21 @@ func NewCallback(fn any) uintptr { | |||
| 52 | 52 | return syscall.NewCallback(fn) | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | + | // CallbackArgs is the integer-class argument block of one C call into a | |
| 56 | + | // NewCallbackInts callback. It is only valid during the callback. | |
| 57 | + | type CallbackArgs struct{} | |
| 58 | + | ||
| 59 | + | // Int returns the i-th integer-class argument (0-based), following the platform C ABI. | |
| 60 | + | func (a *CallbackArgs) Int(i int) uintptr { | |
| 61 | + | panic("purego: NewCallbackInts is not supported on windows") | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // NewCallbackInts returns a C function pointer for an integer-class callback. | |
| 65 | + | // It is not supported on Windows. | |
| 66 | + | func NewCallbackInts(fn func(a *CallbackArgs) uintptr) uintptr { | |
| 67 | + | panic("purego: NewCallbackInts is not supported on windows") | |
| 68 | + | } | |
| 69 | + | ||
| 55 | 70 | // NewCallbackFnPtr converts a Go function pointer to a Windows callback and reuses an existing callback when possible. | |
| 56 | 71 | func NewCallbackFnPtr(fnPtr any) uintptr { | |
| 57 | 72 | val := reflect.ValueOf(fnPtr) | |
| @@ -52,6 +52,21 @@ func NewCallback(fn any) uintptr { | |||||
| 52 | return syscall.NewCallback(fn) | 52 | return syscall.NewCallback(fn) | ||
| 53 | } | 53 | } | ||
| 54 | 54 | ||||
| 55 | + | // CallbackArgs is the integer-class argument block of one C call into a | |||
| 56 | + | // NewCallbackInts callback. It is only valid during the callback. | |||
| 57 | + | type CallbackArgs struct{} | |||
| 58 | + | ||||
| 59 | + | // Int returns the i-th integer-class argument (0-based), following the platform C ABI. | |||
| 60 | + | func (a *CallbackArgs) Int(i int) uintptr { | |||
| 61 | + | panic("purego: NewCallbackInts is not supported on windows") | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // NewCallbackInts returns a C function pointer for an integer-class callback. | |||
| 65 | + | // It is not supported on Windows. | |||
| 66 | + | func NewCallbackInts(fn func(a *CallbackArgs) uintptr) uintptr { | |||
| 67 | + | panic("purego: NewCallbackInts is not supported on windows") | |||
| 68 | + | } | |||
| 69 | + | ||||
| 55 | // NewCallbackFnPtr converts a Go function pointer to a Windows callback and reuses an existing callback when possible. | 70 | // NewCallbackFnPtr converts a Go function pointer to a Windows callback and reuses an existing callback when possible. | ||
| 56 | func NewCallbackFnPtr(fnPtr any) uintptr { | 71 | func NewCallbackFnPtr(fnPtr any) uintptr { | ||
| 57 | val := reflect.ValueOf(fnPtr) | 72 | val := reflect.ValueOf(fnPtr) | ||