detached b335e8a

purego: add callback lifecycle APIs for puregotk

commit
b335e8a0ec07236c8b7b08c09180d6e429e86f0d
author
brice · · 2026-04-15 03:45 UTC
committer
brice · · 2026-09-27 05:39 UTC
parent
1e7d6d6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +216 -10
M callback_test.go +1 -1
@@ -57,7 +57,7 @@ func TestCallbackStringConversion(t *testing.T) {
5757 libFileName := filepath.Join(t.TempDir(), "libcbtest.so")
5858 t.Logf("Build %v", libFileName)
5959
60- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
60+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
6161 t.Fatal(err)
6262 }
6363 defer os.Remove(libFileName)
@@ -57,7 +57,7 @@ func TestCallbackStringConversion(t *testing.T) {
57 libFileName := filepath.Join(t.TempDir(), "libcbtest.so")57 libFileName := filepath.Join(t.TempDir(), "libcbtest.so")
58 t.Logf("Build %v", libFileName)58 t.Logf("Build %v", libFileName)
59 59
60- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {60+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
61 t.Fatal(err)61 t.Fatal(err)
62 }62 }
63 defer os.Remove(libFileName)63 defer os.Remove(libFileName)
A func_sysv_test.go +52 -0
@@ -0,0 +1,52 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd
5+
6+package purego_test
7+
8+import (
9+ "testing"
10+
11+ "github.com/bnema/purego"
12+)
13+
14+func TestUnrefCallback(t *testing.T) {
15+ imp := func() bool { return true }
16+
17+ if err := purego.UnrefCallback(0); err == nil {
18+ t.Fatal("unref of unknown callback returned nil error")
19+ }
20+
21+ ref := purego.NewCallback(imp)
22+ if err := purego.UnrefCallback(ref); err != nil {
23+ t.Fatalf("UnrefCallback(%#x) error = %v", ref, err)
24+ }
25+ if err := purego.UnrefCallback(ref); err == nil {
26+ t.Fatal("second UnrefCallback returned nil error")
27+ }
28+}
29+
30+func TestNewCallbackFnPtrReuseAndUnref(t *testing.T) {
31+ imp := func() bool { return true }
32+
33+ if err := purego.UnrefCallbackFnPtr(&imp); err == nil {
34+ t.Fatal("unref of unknown callback function pointer returned nil error")
35+ }
36+
37+ ref1 := purego.NewCallbackFnPtr(&imp)
38+ ref2 := purego.NewCallbackFnPtr(&imp)
39+ if ref1 != ref2 {
40+ t.Fatalf("NewCallbackFnPtr did not reuse callback: got %#x and %#x", ref1, ref2)
41+ }
42+
43+ if err := purego.UnrefCallbackFnPtr(&imp); err != nil {
44+ t.Fatalf("UnrefCallbackFnPtr error = %v", err)
45+ }
46+ if err := purego.UnrefCallbackFnPtr(&imp); err == nil {
47+ t.Fatal("second UnrefCallbackFnPtr returned nil error")
48+ }
49+ if err := purego.UnrefCallback(ref1); err == nil {
50+ t.Fatal("UnrefCallback on released function-pointer callback returned nil error")
51+ }
52+}
@@ -0,0 +1,52 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd
5+
6+package purego_test
7+
8+import (
9+ "testing"
10+
11+ "github.com/bnema/purego"
12+)
13+
14+func TestUnrefCallback(t *testing.T) {
15+ imp := func() bool { return true }
16+
17+ if err := purego.UnrefCallback(0); err == nil {
18+ t.Fatal("unref of unknown callback returned nil error")
19+ }
20+
21+ ref := purego.NewCallback(imp)
22+ if err := purego.UnrefCallback(ref); err != nil {
23+ t.Fatalf("UnrefCallback(%#x) error = %v", ref, err)
24+ }
25+ if err := purego.UnrefCallback(ref); err == nil {
26+ t.Fatal("second UnrefCallback returned nil error")
27+ }
28+}
29+
30+func TestNewCallbackFnPtrReuseAndUnref(t *testing.T) {
31+ imp := func() bool { return true }
32+
33+ if err := purego.UnrefCallbackFnPtr(&imp); err == nil {
34+ t.Fatal("unref of unknown callback function pointer returned nil error")
35+ }
36+
37+ ref1 := purego.NewCallbackFnPtr(&imp)
38+ ref2 := purego.NewCallbackFnPtr(&imp)
39+ if ref1 != ref2 {
40+ t.Fatalf("NewCallbackFnPtr did not reuse callback: got %#x and %#x", ref1, ref2)
41+ }
42+
43+ if err := purego.UnrefCallbackFnPtr(&imp); err != nil {
44+ t.Fatalf("UnrefCallbackFnPtr error = %v", err)
45+ }
46+ if err := purego.UnrefCallbackFnPtr(&imp); err == nil {
47+ t.Fatal("second UnrefCallbackFnPtr returned nil error")
48+ }
49+ if err := purego.UnrefCallback(ref1); err == nil {
50+ t.Fatal("UnrefCallback on released function-pointer callback returned nil error")
51+ }
52+}
M syscall_unix.go +105 -9
@@ -6,6 +6,7 @@
66 package purego
77
88 import (
9+ "errors"
910 "math"
1011 "reflect"
1112 "runtime"
@@ -49,14 +50,98 @@ func NewCallback(fn any) uintptr {
4950 return compileCallback(fn)
5051 }
5152
53+// NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention.
54+// Calling this function multiple times with the same function pointer returns the original callback address.
55+func NewCallbackFnPtr(fnPtr any) uintptr {
56+ val := reflect.ValueOf(fnPtr)
57+ if val.IsNil() {
58+ panic("purego: function must not be nil")
59+ }
60+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
61+ panic("purego: the type must be a function pointer but was not")
62+ }
63+ if addr, ok := getCallbackByFnPtr(val); ok {
64+ return addr
65+ }
66+ addr := compileCallback(val.Elem().Interface())
67+ cbs.lock.Lock()
68+ cbs.knownFnPtr[val.Pointer()] = addr
69+ if idx, ok := cbs.knownIdx[addr]; ok {
70+ cbs.fnPtrKeys[idx] = val.Pointer()
71+ }
72+ cbs.lock.Unlock()
73+ return addr
74+}
75+
76+// UnrefCallback unreferences a callback created by NewCallback or NewCallbackFnPtr and frees its slot.
77+func UnrefCallback(cb uintptr) error {
78+ cbs.lock.Lock()
79+ defer cbs.lock.Unlock()
80+ idx, ok := cbs.knownIdx[cb]
81+ if !ok {
82+ return errors.New("callback not found")
83+ }
84+ if key := cbs.fnPtrKeys[idx]; key != 0 {
85+ delete(cbs.knownFnPtr, key)
86+ cbs.fnPtrKeys[idx] = 0
87+ }
88+ delete(cbs.knownIdx, cb)
89+ cbs.holes[idx] = struct{}{}
90+ cbs.funcs[idx] = reflect.Value{}
91+ return nil
92+}
93+
94+// UnrefCallbackFnPtr unreferences a callback previously created via NewCallbackFnPtr.
95+func UnrefCallbackFnPtr(fnPtr any) error {
96+ val := reflect.ValueOf(fnPtr)
97+ if val.IsNil() {
98+ panic("purego: function must not be nil")
99+ }
100+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
101+ panic("purego: the type must be a function pointer but was not")
102+ }
103+ cbs.lock.Lock()
104+ defer cbs.lock.Unlock()
105+ addr, ok := cbs.knownFnPtr[val.Pointer()]
106+ if !ok {
107+ return errors.New("callback not found")
108+ }
109+ idx, ok := cbs.knownIdx[addr]
110+ if !ok {
111+ delete(cbs.knownFnPtr, val.Pointer())
112+ return errors.New("callback not found")
113+ }
114+ delete(cbs.knownFnPtr, val.Pointer())
115+ delete(cbs.knownIdx, addr)
116+ cbs.fnPtrKeys[idx] = 0
117+ cbs.holes[idx] = struct{}{}
118+ cbs.funcs[idx] = reflect.Value{}
119+ return nil
120+}
121+
52122 // maxCb is the maximum number of callbacks
53123 // only increase this if you have added more to the callbackasm function
54124 const maxCB = 2000
55125
56-var cbs struct {
57- lock sync.Mutex
58- numFn int // the number of functions currently in cbs.funcs
59- funcs [maxCB]reflect.Value // the saved callbacks
126+var cbs = struct {
127+ lock sync.RWMutex
128+ numFn int // the highest allocated callback index + 1
129+ holes map[int]struct{} // reusable callback slots
130+ funcs [maxCB]reflect.Value // the saved callbacks
131+ knownIdx map[uintptr]int // callback address -> slot index
132+ knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
133+ fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address
134+}{
135+ holes: make(map[int]struct{}),
136+ knownIdx: make(map[uintptr]int, maxCB),
137+ knownFnPtr: make(map[uintptr]uintptr, maxCB),
138+}
139+
140+func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
141+ cbs.lock.RLock()
142+ defer cbs.lock.RUnlock()
143+ addr, ok := cbs.knownFnPtr[val.Pointer()]
144+ return addr, ok
60145 }
61146
62147 func compileCallback(fn any) uintptr {
@@ -103,12 +188,23 @@ output:
103188 }
104189 cbs.lock.Lock()
105190 defer cbs.lock.Unlock()
106- if cbs.numFn >= maxCB {
107- panic("purego: the maximum number of callbacks has been reached")
191+ index := -1
192+ for i := range cbs.holes {
193+ index = i
194+ delete(cbs.holes, i)
195+ break
196+ }
197+ if index < 0 {
198+ if cbs.numFn >= maxCB {
199+ panic("purego: the maximum number of callbacks has been reached")
200+ }
201+ index = cbs.numFn
202+ cbs.numFn++
108203 }
109- cbs.funcs[cbs.numFn] = val
110- cbs.numFn++
111- return callbackasmAddr(cbs.numFn - 1)
204+ cbs.funcs[index] = val
205+ addr := callbackasmAddr(index)
206+ cbs.knownIdx[addr] = index
207+ return addr
112208 }
113209
114210 const ptrSize = unsafe.Sizeof((*int)(nil))
@@ -6,6 +6,7 @@
6 package purego6 package purego
7 7
8 import (8 import (
9+ "errors"
9 "math"10 "math"
10 "reflect"11 "reflect"
11 "runtime"12 "runtime"
@@ -49,14 +50,98 @@ func NewCallback(fn any) uintptr {
49 return compileCallback(fn)50 return compileCallback(fn)
50 }51 }
51 52
53+// NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention.
54+// Calling this function multiple times with the same function pointer returns the original callback address.
55+func NewCallbackFnPtr(fnPtr any) uintptr {
56+ val := reflect.ValueOf(fnPtr)
57+ if val.IsNil() {
58+ panic("purego: function must not be nil")
59+ }
60+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
61+ panic("purego: the type must be a function pointer but was not")
62+ }
63+ if addr, ok := getCallbackByFnPtr(val); ok {
64+ return addr
65+ }
66+ addr := compileCallback(val.Elem().Interface())
67+ cbs.lock.Lock()
68+ cbs.knownFnPtr[val.Pointer()] = addr
69+ if idx, ok := cbs.knownIdx[addr]; ok {
70+ cbs.fnPtrKeys[idx] = val.Pointer()
71+ }
72+ cbs.lock.Unlock()
73+ return addr
74+}
75+
76+// UnrefCallback unreferences a callback created by NewCallback or NewCallbackFnPtr and frees its slot.
77+func UnrefCallback(cb uintptr) error {
78+ cbs.lock.Lock()
79+ defer cbs.lock.Unlock()
80+ idx, ok := cbs.knownIdx[cb]
81+ if !ok {
82+ return errors.New("callback not found")
83+ }
84+ if key := cbs.fnPtrKeys[idx]; key != 0 {
85+ delete(cbs.knownFnPtr, key)
86+ cbs.fnPtrKeys[idx] = 0
87+ }
88+ delete(cbs.knownIdx, cb)
89+ cbs.holes[idx] = struct{}{}
90+ cbs.funcs[idx] = reflect.Value{}
91+ return nil
92+}
93+
94+// UnrefCallbackFnPtr unreferences a callback previously created via NewCallbackFnPtr.
95+func UnrefCallbackFnPtr(fnPtr any) error {
96+ val := reflect.ValueOf(fnPtr)
97+ if val.IsNil() {
98+ panic("purego: function must not be nil")
99+ }
100+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
101+ panic("purego: the type must be a function pointer but was not")
102+ }
103+ cbs.lock.Lock()
104+ defer cbs.lock.Unlock()
105+ addr, ok := cbs.knownFnPtr[val.Pointer()]
106+ if !ok {
107+ return errors.New("callback not found")
108+ }
109+ idx, ok := cbs.knownIdx[addr]
110+ if !ok {
111+ delete(cbs.knownFnPtr, val.Pointer())
112+ return errors.New("callback not found")
113+ }
114+ delete(cbs.knownFnPtr, val.Pointer())
115+ delete(cbs.knownIdx, addr)
116+ cbs.fnPtrKeys[idx] = 0
117+ cbs.holes[idx] = struct{}{}
118+ cbs.funcs[idx] = reflect.Value{}
119+ return nil
120+}
121+
52 // maxCb is the maximum number of callbacks122 // maxCb is the maximum number of callbacks
53 // only increase this if you have added more to the callbackasm function123 // only increase this if you have added more to the callbackasm function
54 const maxCB = 2000124 const maxCB = 2000
55 125
56-var cbs struct {126+var cbs = struct {
57- lock sync.Mutex127+ lock sync.RWMutex
58- numFn int // the number of functions currently in cbs.funcs128+ numFn int // the highest allocated callback index + 1
59- funcs [maxCB]reflect.Value // the saved callbacks129+ holes map[int]struct{} // reusable callback slots
130+ funcs [maxCB]reflect.Value // the saved callbacks
131+ knownIdx map[uintptr]int // callback address -> slot index
132+ knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
133+ fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address
134+}{
135+ holes: make(map[int]struct{}),
136+ knownIdx: make(map[uintptr]int, maxCB),
137+ knownFnPtr: make(map[uintptr]uintptr, maxCB),
138+}
139+
140+func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
141+ cbs.lock.RLock()
142+ defer cbs.lock.RUnlock()
143+ addr, ok := cbs.knownFnPtr[val.Pointer()]
144+ return addr, ok
60 }145 }
61 146
62 func compileCallback(fn any) uintptr {147 func compileCallback(fn any) uintptr {
@@ -103,12 +188,23 @@ output:
103 }188 }
104 cbs.lock.Lock()189 cbs.lock.Lock()
105 defer cbs.lock.Unlock()190 defer cbs.lock.Unlock()
106- if cbs.numFn >= maxCB {191+ index := -1
107- panic("purego: the maximum number of callbacks has been reached")192+ for i := range cbs.holes {
193+ index = i
194+ delete(cbs.holes, i)
195+ break
196+ }
197+ if index < 0 {
198+ if cbs.numFn >= maxCB {
199+ panic("purego: the maximum number of callbacks has been reached")
200+ }
201+ index = cbs.numFn
202+ cbs.numFn++
108 }203 }
109- cbs.funcs[cbs.numFn] = val204+ cbs.funcs[index] = val
110- cbs.numFn++205+ addr := callbackasmAddr(index)
111- return callbackasmAddr(cbs.numFn - 1)206+ cbs.knownIdx[addr] = index
207+ return addr
112 }208 }
113 209
114 const ptrSize = unsafe.Sizeof((*int)(nil))210 const ptrSize = unsafe.Sizeof((*int)(nil))
M syscall_windows.go +58 -0
@@ -4,7 +4,9 @@
44 package purego
55
66 import (
7+ "errors"
78 "reflect"
9+ "sync"
810 "syscall"
911 "unsafe"
1012 )
@@ -50,6 +52,62 @@ func NewCallback(fn any) uintptr {
5052 return syscall.NewCallback(fn)
5153 }
5254
55+// NewCallbackFnPtr converts a Go function pointer to a Windows callback and reuses an existing callback when possible.
56+func NewCallbackFnPtr(fnPtr any) uintptr {
57+ val := reflect.ValueOf(fnPtr)
58+ if val.IsNil() {
59+ panic("purego: function must not be nil")
60+ }
61+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
62+ panic("purego: the type must be a function pointer but was not")
63+ }
64+ if addr, ok := getCallbackByFnPtr(val); ok {
65+ return addr
66+ }
67+ addr := NewCallback(val.Elem().Interface())
68+ cbs.lock.Lock()
69+ cbs.knownFnPtr[val.Pointer()] = addr
70+ cbs.lock.Unlock()
71+ return addr
72+}
73+
74+// UnrefCallback is unsupported on Windows because runtime callback slots cannot be reclaimed.
75+func UnrefCallback(cb uintptr) error {
76+ if cb == 0 {
77+ return errors.New("callback not found")
78+ }
79+ return errors.New("purego: unreferencing callbacks is unsupported on windows")
80+}
81+
82+// UnrefCallbackFnPtr is unsupported on Windows because runtime callback slots cannot be reclaimed.
83+func UnrefCallbackFnPtr(fnPtr any) error {
84+ val := reflect.ValueOf(fnPtr)
85+ if val.IsNil() {
86+ panic("purego: function must not be nil")
87+ }
88+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
89+ panic("purego: the type must be a function pointer but was not")
90+ }
91+ return errors.New("purego: unreferencing callbacks is unsupported on windows")
92+}
93+
94+// maxCb is the maximum number of tracked Windows callback function pointers.
95+const maxCB = 1024
96+
97+var cbs = struct {
98+ lock sync.RWMutex
99+ knownFnPtr map[uintptr]uintptr
100+}{
101+ knownFnPtr: make(map[uintptr]uintptr, maxCB),
102+}
103+
104+func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
105+ cbs.lock.RLock()
106+ defer cbs.lock.RUnlock()
107+ addr, ok := cbs.knownFnPtr[val.Pointer()]
108+ return addr, ok
109+}
110+
53111 func loadSymbol(handle uintptr, name string) (uintptr, error) {
54112 return syscall.GetProcAddress(syscall.Handle(handle), name)
55113 }
@@ -4,7 +4,9 @@
4 package purego4 package purego
5 5
6 import (6 import (
7+ "errors"
7 "reflect"8 "reflect"
9+ "sync"
8 "syscall"10 "syscall"
9 "unsafe"11 "unsafe"
10 )12 )
@@ -50,6 +52,62 @@ func NewCallback(fn any) uintptr {
50 return syscall.NewCallback(fn)52 return syscall.NewCallback(fn)
51 }53 }
52 54
55+// NewCallbackFnPtr converts a Go function pointer to a Windows callback and reuses an existing callback when possible.
56+func NewCallbackFnPtr(fnPtr any) uintptr {
57+ val := reflect.ValueOf(fnPtr)
58+ if val.IsNil() {
59+ panic("purego: function must not be nil")
60+ }
61+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
62+ panic("purego: the type must be a function pointer but was not")
63+ }
64+ if addr, ok := getCallbackByFnPtr(val); ok {
65+ return addr
66+ }
67+ addr := NewCallback(val.Elem().Interface())
68+ cbs.lock.Lock()
69+ cbs.knownFnPtr[val.Pointer()] = addr
70+ cbs.lock.Unlock()
71+ return addr
72+}
73+
74+// UnrefCallback is unsupported on Windows because runtime callback slots cannot be reclaimed.
75+func UnrefCallback(cb uintptr) error {
76+ if cb == 0 {
77+ return errors.New("callback not found")
78+ }
79+ return errors.New("purego: unreferencing callbacks is unsupported on windows")
80+}
81+
82+// UnrefCallbackFnPtr is unsupported on Windows because runtime callback slots cannot be reclaimed.
83+func UnrefCallbackFnPtr(fnPtr any) error {
84+ val := reflect.ValueOf(fnPtr)
85+ if val.IsNil() {
86+ panic("purego: function must not be nil")
87+ }
88+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
89+ panic("purego: the type must be a function pointer but was not")
90+ }
91+ return errors.New("purego: unreferencing callbacks is unsupported on windows")
92+}
93+
94+// maxCb is the maximum number of tracked Windows callback function pointers.
95+const maxCB = 1024
96+
97+var cbs = struct {
98+ lock sync.RWMutex
99+ knownFnPtr map[uintptr]uintptr
100+}{
101+ knownFnPtr: make(map[uintptr]uintptr, maxCB),
102+}
103+
104+func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
105+ cbs.lock.RLock()
106+ defer cbs.lock.RUnlock()
107+ addr, ok := cbs.knownFnPtr[val.Pointer()]
108+ return addr, ok
109+}
110+
53 func loadSymbol(handle uintptr, name string) (uintptr, error) {111 func loadSymbol(handle uintptr, name string) (uintptr, error) {
54 return syscall.GetProcAddress(syscall.Handle(handle), name)112 return syscall.GetProcAddress(syscall.Handle(handle), name)
55 }113 }

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