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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 57 | 57 | libFileName := filepath.Join(t.TempDir(), "libcbtest.so") | |
| 58 | 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 | 61 | t.Fatal(err) | |
| 62 | 62 | } | |
| 63 | 63 | 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 @@ | |||
| 6 | 6 | package purego | |
| 7 | 7 | ||
| 8 | 8 | import ( | |
| 9 | + | "errors" | |
| 9 | 10 | "math" | |
| 10 | 11 | "reflect" | |
| 11 | 12 | "runtime" | |
| @@ -49,14 +50,98 @@ func NewCallback(fn any) uintptr { | |||
| 49 | 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 | 122 | // maxCb is the maximum number of callbacks | |
| 53 | 123 | // only increase this if you have added more to the callbackasm function | |
| 54 | 124 | const maxCB = 2000 | |
| 55 | 125 | ||
| 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 | |
| 60 | 145 | } | |
| 61 | 146 | ||
| 62 | 147 | func compileCallback(fn any) uintptr { | |
| @@ -103,12 +188,23 @@ output: | |||
| 103 | 188 | } | |
| 104 | 189 | cbs.lock.Lock() | |
| 105 | 190 | 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++ | |
| 108 | 203 | } | |
| 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 | |
| 112 | 208 | } | |
| 113 | 209 | ||
| 114 | 210 | const ptrSize = unsafe.Sizeof((*int)(nil)) | |
| @@ -6,6 +6,7 @@ | |||||
| 6 | package purego | 6 | 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 callbacks | 122 | // maxCb is the maximum number of callbacks | ||
| 53 | // only increase this if you have added more to the callbackasm function | 123 | // only increase this if you have added more to the callbackasm function | ||
| 54 | const maxCB = 2000 | 124 | const maxCB = 2000 | ||
| 55 | 125 | ||||
| 56 | - | var cbs struct { | 126 | + | var cbs = struct { |
| 57 | - | lock sync.Mutex | 127 | + | lock sync.RWMutex |
| 58 | - | numFn int // the number of functions currently in cbs.funcs | 128 | + | numFn int // the highest allocated callback index + 1 |
| 59 | - | funcs [maxCB]reflect.Value // the saved callbacks | 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 | |||
| 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] = val | 204 | + | 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 @@ | |||
| 4 | 4 | package purego | |
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | + | "errors" | |
| 7 | 8 | "reflect" | |
| 9 | + | "sync" | |
| 8 | 10 | "syscall" | |
| 9 | 11 | "unsafe" | |
| 10 | 12 | ) | |
| @@ -50,6 +52,62 @@ func NewCallback(fn any) uintptr { | |||
| 50 | 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 | 111 | func loadSymbol(handle uintptr, name string) (uintptr, error) { | |
| 54 | 112 | return syscall.GetProcAddress(syscall.Handle(handle), name) | |
| 55 | 113 | } | |
| @@ -4,7 +4,9 @@ | |||||
| 4 | package purego | 4 | 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 | } | ||