detached c5aa04a

purego: add callback lifecycle APIs for puregotk

commit
c5aa04a18a1be9324cf49b46b7e92b7d854ba8c3
author
brice · · 2026-04-15 03:45 UTC
parent
8b0a311
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
tag: v0.11.0-bnema.2
3 files · +217 -10
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 +107 -10
@@ -7,6 +7,7 @@
77 package purego
88
99 import (
10+ "errors"
1011 "reflect"
1112 "runtime"
1213 "sync"
@@ -52,15 +53,101 @@ func NewCallback(fn any) uintptr {
5253 return compileCallback(fn)
5354 }
5455
56+// NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention.
57+// Calling this function multiple times with the same function pointer returns the original callback address.
58+func NewCallbackFnPtr(fnPtr any) uintptr {
59+ val := reflect.ValueOf(fnPtr)
60+ if val.IsNil() {
61+ panic("purego: function must not be nil")
62+ }
63+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
64+ panic("purego: the type must be a function pointer but was not")
65+ }
66+ if addr, ok := getCallbackByFnPtr(val); ok {
67+ return addr
68+ }
69+ addr := compileCallback(val.Elem().Interface())
70+ cbs.lock.Lock()
71+ cbs.knownFnPtr[val.Pointer()] = addr
72+ if idx, ok := cbs.knownIdx[addr]; ok {
73+ cbs.fnPtrKeys[idx] = val.Pointer()
74+ }
75+ cbs.lock.Unlock()
76+ return addr
77+}
78+
79+// UnrefCallback unreferences a callback created by NewCallback or NewCallbackFnPtr and frees its slot.
80+func UnrefCallback(cb uintptr) error {
81+ cbs.lock.Lock()
82+ defer cbs.lock.Unlock()
83+ idx, ok := cbs.knownIdx[cb]
84+ if !ok {
85+ return errors.New("callback not found")
86+ }
87+ if key := cbs.fnPtrKeys[idx]; key != 0 {
88+ delete(cbs.knownFnPtr, key)
89+ cbs.fnPtrKeys[idx] = 0
90+ }
91+ delete(cbs.knownIdx, cb)
92+ cbs.holes[idx] = struct{}{}
93+ cbs.funcs[idx] = reflect.Value{}
94+ cbs.argPools[idx] = nil
95+ return nil
96+}
97+
98+// UnrefCallbackFnPtr unreferences a callback previously created via NewCallbackFnPtr.
99+func UnrefCallbackFnPtr(fnPtr any) error {
100+ val := reflect.ValueOf(fnPtr)
101+ if val.IsNil() {
102+ panic("purego: function must not be nil")
103+ }
104+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
105+ panic("purego: the type must be a function pointer but was not")
106+ }
107+ cbs.lock.Lock()
108+ defer cbs.lock.Unlock()
109+ addr, ok := cbs.knownFnPtr[val.Pointer()]
110+ if !ok {
111+ return errors.New("callback not found")
112+ }
113+ idx, ok := cbs.knownIdx[addr]
114+ if !ok {
115+ delete(cbs.knownFnPtr, val.Pointer())
116+ return errors.New("callback not found")
117+ }
118+ delete(cbs.knownFnPtr, val.Pointer())
119+ delete(cbs.knownIdx, addr)
120+ cbs.fnPtrKeys[idx] = 0
121+ cbs.holes[idx] = struct{}{}
122+ cbs.funcs[idx] = reflect.Value{}
123+ cbs.argPools[idx] = nil
124+ return nil
125+}
126+
55127 // maxCb is the maximum number of callbacks
56128 // only increase this if you have added more to the callbackasm function
57129 const maxCB = 2000
58130
59-var cbs struct {
60- lock sync.RWMutex
61- numFn int // the number of functions currently in cbs.funcs
62- funcs [maxCB]reflect.Value // the saved callbacks
63- argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback
131+var cbs = struct {
132+ lock sync.RWMutex
133+ numFn int // the highest allocated callback index + 1
134+ holes map[int]struct{} // reusable callback slots
135+ funcs [maxCB]reflect.Value // the saved callbacks
136+ argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback
137+ knownIdx map[uintptr]int // callback address -> slot index
138+ knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
139+ fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address
140+}{
141+ holes: make(map[int]struct{}),
142+ knownIdx: make(map[uintptr]int, maxCB),
143+ knownFnPtr: make(map[uintptr]uintptr, maxCB),
144+}
145+
146+func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
147+ cbs.lock.RLock()
148+ defer cbs.lock.RUnlock()
149+ addr, ok := cbs.knownFnPtr[val.Pointer()]
150+ return addr, ok
64151 }
65152
66153 func compileCallback(fn any) uintptr {
@@ -103,10 +190,19 @@ output:
103190 }
104191 cbs.lock.Lock()
105192 defer cbs.lock.Unlock()
106- if cbs.numFn >= maxCB {
107- panic("purego: the maximum number of callbacks has been reached")
193+ index := -1
194+ for i := range cbs.holes {
195+ index = i
196+ delete(cbs.holes, i)
197+ break
198+ }
199+ if index < 0 {
200+ if cbs.numFn >= maxCB {
201+ panic("purego: the maximum number of callbacks has been reached")
202+ }
203+ index = cbs.numFn
204+ cbs.numFn++
108205 }
109- index := cbs.numFn
110206 cbs.funcs[index] = val
111207 numIn := ty.NumIn()
112208 cbs.argPools[index] = &sync.Pool{
@@ -114,8 +210,9 @@ output:
114210 return make([]reflect.Value, numIn)
115211 },
116212 }
117- cbs.numFn++
118- return callbackasmAddr(index)
213+ addr := callbackasmAddr(index)
214+ cbs.knownIdx[addr] = index
215+ return addr
119216 }
120217
121218 const ptrSize = unsafe.Sizeof((*int)(nil))
@@ -7,6 +7,7 @@
7 package purego7 package purego
8 8
9 import (9 import (
10+ "errors"
10 "reflect"11 "reflect"
11 "runtime"12 "runtime"
12 "sync"13 "sync"
@@ -52,15 +53,101 @@ func NewCallback(fn any) uintptr {
52 return compileCallback(fn)53 return compileCallback(fn)
53 }54 }
54 55
56+// NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention.
57+// Calling this function multiple times with the same function pointer returns the original callback address.
58+func NewCallbackFnPtr(fnPtr any) uintptr {
59+ val := reflect.ValueOf(fnPtr)
60+ if val.IsNil() {
61+ panic("purego: function must not be nil")
62+ }
63+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
64+ panic("purego: the type must be a function pointer but was not")
65+ }
66+ if addr, ok := getCallbackByFnPtr(val); ok {
67+ return addr
68+ }
69+ addr := compileCallback(val.Elem().Interface())
70+ cbs.lock.Lock()
71+ cbs.knownFnPtr[val.Pointer()] = addr
72+ if idx, ok := cbs.knownIdx[addr]; ok {
73+ cbs.fnPtrKeys[idx] = val.Pointer()
74+ }
75+ cbs.lock.Unlock()
76+ return addr
77+}
78+
79+// UnrefCallback unreferences a callback created by NewCallback or NewCallbackFnPtr and frees its slot.
80+func UnrefCallback(cb uintptr) error {
81+ cbs.lock.Lock()
82+ defer cbs.lock.Unlock()
83+ idx, ok := cbs.knownIdx[cb]
84+ if !ok {
85+ return errors.New("callback not found")
86+ }
87+ if key := cbs.fnPtrKeys[idx]; key != 0 {
88+ delete(cbs.knownFnPtr, key)
89+ cbs.fnPtrKeys[idx] = 0
90+ }
91+ delete(cbs.knownIdx, cb)
92+ cbs.holes[idx] = struct{}{}
93+ cbs.funcs[idx] = reflect.Value{}
94+ cbs.argPools[idx] = nil
95+ return nil
96+}
97+
98+// UnrefCallbackFnPtr unreferences a callback previously created via NewCallbackFnPtr.
99+func UnrefCallbackFnPtr(fnPtr any) error {
100+ val := reflect.ValueOf(fnPtr)
101+ if val.IsNil() {
102+ panic("purego: function must not be nil")
103+ }
104+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
105+ panic("purego: the type must be a function pointer but was not")
106+ }
107+ cbs.lock.Lock()
108+ defer cbs.lock.Unlock()
109+ addr, ok := cbs.knownFnPtr[val.Pointer()]
110+ if !ok {
111+ return errors.New("callback not found")
112+ }
113+ idx, ok := cbs.knownIdx[addr]
114+ if !ok {
115+ delete(cbs.knownFnPtr, val.Pointer())
116+ return errors.New("callback not found")
117+ }
118+ delete(cbs.knownFnPtr, val.Pointer())
119+ delete(cbs.knownIdx, addr)
120+ cbs.fnPtrKeys[idx] = 0
121+ cbs.holes[idx] = struct{}{}
122+ cbs.funcs[idx] = reflect.Value{}
123+ cbs.argPools[idx] = nil
124+ return nil
125+}
126+
55 // maxCb is the maximum number of callbacks127 // maxCb is the maximum number of callbacks
56 // only increase this if you have added more to the callbackasm function128 // only increase this if you have added more to the callbackasm function
57 const maxCB = 2000129 const maxCB = 2000
58 130
59-var cbs struct {131+var cbs = struct {
60- lock sync.RWMutex132+ lock sync.RWMutex
61- numFn int // the number of functions currently in cbs.funcs133+ numFn int // the highest allocated callback index + 1
62- funcs [maxCB]reflect.Value // the saved callbacks134+ holes map[int]struct{} // reusable callback slots
63- argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback135+ funcs [maxCB]reflect.Value // the saved callbacks
136+ argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback
137+ knownIdx map[uintptr]int // callback address -> slot index
138+ knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
139+ fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address
140+}{
141+ holes: make(map[int]struct{}),
142+ knownIdx: make(map[uintptr]int, maxCB),
143+ knownFnPtr: make(map[uintptr]uintptr, maxCB),
144+}
145+
146+func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
147+ cbs.lock.RLock()
148+ defer cbs.lock.RUnlock()
149+ addr, ok := cbs.knownFnPtr[val.Pointer()]
150+ return addr, ok
64 }151 }
65 152
66 func compileCallback(fn any) uintptr {153 func compileCallback(fn any) uintptr {
@@ -103,10 +190,19 @@ output:
103 }190 }
104 cbs.lock.Lock()191 cbs.lock.Lock()
105 defer cbs.lock.Unlock()192 defer cbs.lock.Unlock()
106- if cbs.numFn >= maxCB {193+ index := -1
107- panic("purego: the maximum number of callbacks has been reached")194+ for i := range cbs.holes {
195+ index = i
196+ delete(cbs.holes, i)
197+ break
198+ }
199+ if index < 0 {
200+ if cbs.numFn >= maxCB {
201+ panic("purego: the maximum number of callbacks has been reached")
202+ }
203+ index = cbs.numFn
204+ cbs.numFn++
108 }205 }
109- index := cbs.numFn
110 cbs.funcs[index] = val206 cbs.funcs[index] = val
111 numIn := ty.NumIn()207 numIn := ty.NumIn()
112 cbs.argPools[index] = &sync.Pool{208 cbs.argPools[index] = &sync.Pool{
@@ -114,8 +210,9 @@ output:
114 return make([]reflect.Value, numIn)210 return make([]reflect.Value, numIn)
115 },211 },
116 }212 }
117- cbs.numFn++213+ addr := callbackasmAddr(index)
118- return callbackasmAddr(index)214+ cbs.knownIdx[addr] = index
215+ return addr
119 }216 }
120 217
121 const ptrSize = unsafe.Sizeof((*int)(nil))218 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 )
@@ -42,6 +44,62 @@ func NewCallback(fn any) uintptr {
4244 return syscall.NewCallback(fn)
4345 }
4446
47+// NewCallbackFnPtr converts a Go function pointer to a Windows callback and reuses an existing callback when possible.
48+func NewCallbackFnPtr(fnPtr any) uintptr {
49+ val := reflect.ValueOf(fnPtr)
50+ if val.IsNil() {
51+ panic("purego: function must not be nil")
52+ }
53+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
54+ panic("purego: the type must be a function pointer but was not")
55+ }
56+ if addr, ok := getCallbackByFnPtr(val); ok {
57+ return addr
58+ }
59+ addr := NewCallback(val.Elem().Interface())
60+ cbs.lock.Lock()
61+ cbs.knownFnPtr[val.Pointer()] = addr
62+ cbs.lock.Unlock()
63+ return addr
64+}
65+
66+// UnrefCallback is unsupported on Windows because runtime callback slots cannot be reclaimed.
67+func UnrefCallback(cb uintptr) error {
68+ if cb == 0 {
69+ return errors.New("callback not found")
70+ }
71+ return errors.New("purego: unreferencing callbacks is unsupported on windows")
72+}
73+
74+// UnrefCallbackFnPtr is unsupported on Windows because runtime callback slots cannot be reclaimed.
75+func UnrefCallbackFnPtr(fnPtr any) error {
76+ val := reflect.ValueOf(fnPtr)
77+ if val.IsNil() {
78+ panic("purego: function must not be nil")
79+ }
80+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
81+ panic("purego: the type must be a function pointer but was not")
82+ }
83+ return errors.New("purego: unreferencing callbacks is unsupported on windows")
84+}
85+
86+// maxCb is the maximum number of tracked Windows callback function pointers.
87+const maxCB = 1024
88+
89+var cbs = struct {
90+ lock sync.RWMutex
91+ knownFnPtr map[uintptr]uintptr
92+}{
93+ knownFnPtr: make(map[uintptr]uintptr, maxCB),
94+}
95+
96+func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
97+ cbs.lock.RLock()
98+ defer cbs.lock.RUnlock()
99+ addr, ok := cbs.knownFnPtr[val.Pointer()]
100+ return addr, ok
101+}
102+
45103 func loadSymbol(handle uintptr, name string) (uintptr, error) {
46104 return syscall.GetProcAddress(syscall.Handle(handle), name)
47105 }
@@ -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 )
@@ -42,6 +44,62 @@ func NewCallback(fn any) uintptr {
42 return syscall.NewCallback(fn)44 return syscall.NewCallback(fn)
43 }45 }
44 46
47+// NewCallbackFnPtr converts a Go function pointer to a Windows callback and reuses an existing callback when possible.
48+func NewCallbackFnPtr(fnPtr any) uintptr {
49+ val := reflect.ValueOf(fnPtr)
50+ if val.IsNil() {
51+ panic("purego: function must not be nil")
52+ }
53+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
54+ panic("purego: the type must be a function pointer but was not")
55+ }
56+ if addr, ok := getCallbackByFnPtr(val); ok {
57+ return addr
58+ }
59+ addr := NewCallback(val.Elem().Interface())
60+ cbs.lock.Lock()
61+ cbs.knownFnPtr[val.Pointer()] = addr
62+ cbs.lock.Unlock()
63+ return addr
64+}
65+
66+// UnrefCallback is unsupported on Windows because runtime callback slots cannot be reclaimed.
67+func UnrefCallback(cb uintptr) error {
68+ if cb == 0 {
69+ return errors.New("callback not found")
70+ }
71+ return errors.New("purego: unreferencing callbacks is unsupported on windows")
72+}
73+
74+// UnrefCallbackFnPtr is unsupported on Windows because runtime callback slots cannot be reclaimed.
75+func UnrefCallbackFnPtr(fnPtr any) error {
76+ val := reflect.ValueOf(fnPtr)
77+ if val.IsNil() {
78+ panic("purego: function must not be nil")
79+ }
80+ if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
81+ panic("purego: the type must be a function pointer but was not")
82+ }
83+ return errors.New("purego: unreferencing callbacks is unsupported on windows")
84+}
85+
86+// maxCb is the maximum number of tracked Windows callback function pointers.
87+const maxCB = 1024
88+
89+var cbs = struct {
90+ lock sync.RWMutex
91+ knownFnPtr map[uintptr]uintptr
92+}{
93+ knownFnPtr: make(map[uintptr]uintptr, maxCB),
94+}
95+
96+func getCallbackByFnPtr(val reflect.Value) (uintptr, bool) {
97+ cbs.lock.RLock()
98+ defer cbs.lock.RUnlock()
99+ addr, ok := cbs.knownFnPtr[val.Pointer()]
100+ return addr, ok
101+}
102+
45 func loadSymbol(handle uintptr, name string) (uintptr, error) {103 func loadSymbol(handle uintptr, name string) (uintptr, error) {
46 return syscall.GetProcAddress(syscall.Handle(handle), name)104 return syscall.GetProcAddress(syscall.Handle(handle), name)
47 }105 }

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