detached f844878

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
5656 return compileCallback(fn)
5757 }
5858
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+
5993 // NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention.
6094 // Calling this function multiple times with the same function pointer returns the original callback address.
6195 func NewCallbackFnPtr(fnPtr any) uintptr {
@@ -101,6 +135,7 @@ func UnrefCallback(cb uintptr) error {
101135 delete(cbs.knownIdx, cb)
102136 cbs.holes[idx] = struct{}{}
103137 cbs.funcs[idx] = reflect.Value{}
138+ cbs.fast[idx] = nil
104139 recordCallbackLedger("release", idx, cb, key, val, false, "")
105140 return nil
106141 }
@@ -143,13 +178,14 @@ const maxCB = 2000
143178
144179 var cbs = struct {
145180 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
153189 }{
154190 holes: make(map[int]struct{}),
155191 knownIdx: make(map[uintptr]int, maxCB),
@@ -389,6 +425,20 @@ output:
389425 }
390426 cbs.lock.Lock()
391427 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 {
392442 index := -1
393443 reused := false
394444 for i := range cbs.holes {
@@ -401,16 +451,19 @@ output:
401451 remaining := maxCB - cbs.numFn
402452 if remaining <= 0 {
403453 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+ }
405457 panic("purego: the maximum number of callbacks has been reached")
406458 }
407- if remaining <= 100 || remaining%250 == 0 {
459+ if val.IsValid() && (remaining <= 100 || remaining%250 == 0) {
408460 traceCallbackAllocation("allocated", val, remaining)
409461 }
410462 index = cbs.numFn
411463 cbs.numFn++
412464 }
413465 cbs.funcs[index] = val
466+ cbs.fast[index] = fast
414467 addr := callbackasmAddr(index)
415468 cbs.knownIdx[addr] = index
416469 if reused {
@@ -437,9 +490,22 @@ var callbackasmABI0 = uintptr(unsafe.Pointer(&__callbackasm))
437490 // This closure is used inside sys_darwin_GOARCH.s
438491 var callbackWrap_call = callbackWrap
439492
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+
440498 // callbackWrap is called by assembly code which determines which Go function to call.
441499 // This function takes the arguments and passes them to the Go function and returns the result.
442500 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+ }
443509 cbs.lock.Lock()
444510 fn := cbs.funcs[a.index]
445511 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 nil140 return nil
106 }141 }
@@ -143,13 +178,14 @@ const maxCB = 2000
143 178
144 var cbs = struct {179 var cbs = struct {
145 lock sync.RWMutex180 lock sync.RWMutex
146- numFn int // the highest allocated callback index + 1181+ numFn int // the highest allocated callback index + 1
147- holes map[int]struct{} // reusable callback slots182+ holes map[int]struct{} // reusable callback slots
148- funcs [maxCB]reflect.Value // the saved callbacks183+ funcs [maxCB]reflect.Value // the saved callbacks
149- knownIdx map[uintptr]int // callback address -> slot index184+ fast [maxCB]func(*CallbackArgs) uintptr // integer-only callbacks
150- knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address185+ knownIdx map[uintptr]int // callback address -> slot index
151- fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address186+ knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
152- ledgerSeq uint64 // monotonic callback ledger event sequence187+ 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 := -1442 index := -1
393 reused := false443 reused := false
394 for i := range cbs.holes {444 for i := range cbs.holes {
@@ -401,16 +451,19 @@ output:
401 remaining := maxCB - cbs.numFn451 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.numFn462 index = cbs.numFn
411 cbs.numFn++463 cbs.numFn++
412 }464 }
413 cbs.funcs[index] = val465 cbs.funcs[index] = val
466+ cbs.fast[index] = fast
414 addr := callbackasmAddr(index)467 addr := callbackasmAddr(index)
415 cbs.knownIdx[addr] = index468 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.s490 // This closure is used inside sys_darwin_GOARCH.s
438 var callbackWrap_call = callbackWrap491 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 {
5252 return syscall.NewCallback(fn)
5353 }
5454
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+
5570 // NewCallbackFnPtr converts a Go function pointer to a Windows callback and reuses an existing callback when possible.
5671 func NewCallbackFnPtr(fnPtr any) uintptr {
5772 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)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close