detached 6673afe

purego: document callback exhaustion and reuse (#516)

Document that each NewCallback call creates a new callback, even for
the same function value. Passing func arguments through RegisterFunc
also consumes callbacks on every call and can exhaust the callback
table. Recommend creating callbacks once and reusing their pointers.

Update the documented minimum callback count on Unix to 1024.

Closes #521
commit
6673afeb2d5e9ad7af97ed157ab63c653483e2dc
author
Hiroki KUMAZAKI · · 2026-09-12 07:08 UTC
committer
GitHub · · 2026-09-12 07:08 UTC
parent
9ba4c40
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
2 files · +11 -1
M syscall_unix.go +6 -1
@@ -23,8 +23,13 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
2323 // This is useful when interoperating with C code requiring callbacks. The argument is expected to be a
2424 // function with zero or one uintptr-sized result. The function must not have arguments with size larger than the size
2525 // of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory allocated
26-// for these callbacks is never released. At least 2000 callbacks can always be created. Although this function
26+// for these callbacks is never released. At least 1024 callbacks can always be created. Although this function
2727 // provides similar functionality to windows.NewCallback it is distinct.
28+//
29+// Every call to NewCallback creates a new callback even for the same function value, so passing a Go callback to C
30+// inside a loop (e.g. a qsort comparator) keeps consuming callbacks and eventually panics once they are exhausted.
31+// The same happens when a func value is passed to a C function, as [RegisterFunc] creates a new callback for each
32+// call. Create the callback once with NewCallback and reuse the returned pointer instead.
2833 func NewCallback(fn any) uintptr {
2934 ty := reflect.TypeOf(fn)
3035 if ty == nil || ty.Kind() != reflect.Func {
@@ -23,8 +23,13 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
23 // This is useful when interoperating with C code requiring callbacks. The argument is expected to be a23 // This is useful when interoperating with C code requiring callbacks. The argument is expected to be a
24 // function with zero or one uintptr-sized result. The function must not have arguments with size larger than the size24 // function with zero or one uintptr-sized result. The function must not have arguments with size larger than the size
25 // of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory allocated25 // of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory allocated
26-// for these callbacks is never released. At least 2000 callbacks can always be created. Although this function26+// for these callbacks is never released. At least 1024 callbacks can always be created. Although this function
27 // provides similar functionality to windows.NewCallback it is distinct.27 // provides similar functionality to windows.NewCallback it is distinct.
28+//
29+// Every call to NewCallback creates a new callback even for the same function value, so passing a Go callback to C
30+// inside a loop (e.g. a qsort comparator) keeps consuming callbacks and eventually panics once they are exhausted.
31+// The same happens when a func value is passed to a C function, as [RegisterFunc] creates a new callback for each
32+// call. Create the callback once with NewCallback and reuse the returned pointer instead.
28 func NewCallback(fn any) uintptr {33 func NewCallback(fn any) uintptr {
29 ty := reflect.TypeOf(fn)34 ty := reflect.TypeOf(fn)
30 if ty == nil || ty.Kind() != reflect.Func {35 if ty == nil || ty.Kind() != reflect.Func {
M syscall_windows.go +5 -0
@@ -23,6 +23,11 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
2323 // allocated for these callbacks is never released. Between NewCallback and NewCallbackCDecl, at least 1024
2424 // callbacks can always be created. Although this function is similar to the darwin version it may act
2525 // differently.
26+//
27+// Every call to NewCallback creates a new callback even for the same function value, so passing a Go callback to C
28+// inside a loop (e.g. a qsort comparator) keeps consuming callbacks and eventually panics once they are exhausted.
29+// The same happens when a func value is passed to a C function, as [RegisterFunc] creates a new callback for each
30+// call. Create the callback once with NewCallback and reuse the returned pointer instead.
2631 func NewCallback(fn any) uintptr {
2732 isCDecl := false
2833 ty := reflect.TypeOf(fn)
@@ -23,6 +23,11 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
23 // allocated for these callbacks is never released. Between NewCallback and NewCallbackCDecl, at least 102423 // allocated for these callbacks is never released. Between NewCallback and NewCallbackCDecl, at least 1024
24 // callbacks can always be created. Although this function is similar to the darwin version it may act24 // callbacks can always be created. Although this function is similar to the darwin version it may act
25 // differently.25 // differently.
26+//
27+// Every call to NewCallback creates a new callback even for the same function value, so passing a Go callback to C
28+// inside a loop (e.g. a qsort comparator) keeps consuming callbacks and eventually panics once they are exhausted.
29+// The same happens when a func value is passed to a C function, as [RegisterFunc] creates a new callback for each
30+// call. Create the callback once with NewCallback and reuse the returned pointer instead.
26 func NewCallback(fn any) uintptr {31 func NewCallback(fn any) uintptr {
27 isCDecl := false32 isCDecl := false
28 ty := reflect.TypeOf(fn)33 ty := reflect.TypeOf(fn)

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