1package glib
2
3import (
4 "fmt"
5 "reflect"
6 "sync"
7
8 "github.com/ebitengine/purego"
9 "github.com/bnema/puregotk/pkg/core"
10)
11
12var callbacks = struct {
13 sync.RWMutex
14 refs map[uintptr]uintptr
15 closures map[uintptr]interface{}
16 handlerToCallback map[uint]uintptr
17 sourceToCallback map[uint]uintptr
18 callbackRefCount map[uintptr]int
19}{
20 refs: make(map[uintptr]uintptr),
21 closures: make(map[uintptr]interface{}),
22 handlerToCallback: make(map[uint]uintptr),
23 sourceToCallback: make(map[uint]uintptr),
24 callbackRefCount: make(map[uintptr]int),
25}
26
27// GetCallback retrives a callback reference by value.
28// Users should not need to call this.
29func GetCallback(cbPtr uintptr) (uintptr, bool) {
30 callbacks.RLock()
31 defer callbacks.RUnlock()
32 refPtr, ok := callbacks.refs[cbPtr]
33 return refPtr, ok
34}
35
36// SaveCallback saves a reference to the callback value.
37// Users should not need to call this.
38func SaveCallback(cbPtr uintptr, refPtr uintptr) {
39 callbacks.Lock()
40 callbacks.refs[cbPtr] = refPtr
41 callbacks.Unlock()
42}
43
44// SaveCallbackWithClosure saves a reference to the callback value and retains the
45// provided closure to prevent it from being garbage collected.
46// Users should not need to call this.
47func SaveCallbackWithClosure(cbPtr uintptr, refPtr uintptr, closure interface{}) {
48 callbacks.Lock()
49 callbacks.refs[cbPtr] = refPtr
50 callbacks.closures[cbPtr] = closure
51 if _, ok := callbacks.callbackRefCount[cbPtr]; !ok {
52 callbacks.callbackRefCount[cbPtr] = 1
53 }
54 callbacks.Unlock()
55}
56
57// RemoveCallback removes a callback from the registry, allowing it to be garbage
58// collected.
59// Users should not need to call this.
60func RemoveCallback(cbPtr uintptr) {
61 callbacks.Lock()
62 for handlerID, mappedCbPtr := range callbacks.handlerToCallback {
63 if mappedCbPtr == cbPtr {
64 delete(callbacks.handlerToCallback, handlerID)
65 }
66 }
67 for sourceID, mappedCbPtr := range callbacks.sourceToCallback {
68 if mappedCbPtr == cbPtr {
69 delete(callbacks.sourceToCallback, sourceID)
70 }
71 }
72 delete(callbacks.refs, cbPtr)
73 delete(callbacks.closures, cbPtr)
74 delete(callbacks.callbackRefCount, cbPtr)
75 callbacks.Unlock()
76}
77
78// acquireCallbackRef increments callbackRefCount for cbPtr.
79// Caller must hold callbacks.Lock().
80func acquireCallbackRef(cbPtr uintptr) {
81 callbacks.callbackRefCount[cbPtr]++
82}
83
84func hasCallbackMappings(cbPtr uintptr) bool {
85 for _, mappedCbPtr := range callbacks.handlerToCallback {
86 if mappedCbPtr == cbPtr {
87 return true
88 }
89 }
90 for _, mappedCbPtr := range callbacks.sourceToCallback {
91 if mappedCbPtr == cbPtr {
92 return true
93 }
94 }
95 return false
96}
97
98// releaseCallbackRef decrements callbackRefCount for cbPtr and removes callback
99// data when it reaches zero.
100// Caller must hold callbacks.Lock().
101// Handler/source mappings to cbPtr are expected to be removed or replaced by
102// the caller (RemoveCallbackByHandler, RemoveCallbackBySource,
103// SaveHandlerMapping, SaveSourceMapping).
104func releaseCallbackRef(cbPtr uintptr) {
105 count, ok := callbacks.callbackRefCount[cbPtr]
106 if !ok {
107 return
108 }
109
110 count--
111 if count > 0 {
112 callbacks.callbackRefCount[cbPtr] = count
113 return
114 }
115
116 delete(callbacks.callbackRefCount, cbPtr)
117 delete(callbacks.refs, cbPtr)
118 delete(callbacks.closures, cbPtr)
119}
120
121// SaveHandlerMapping records a signal handler ID → callback pointer mapping
122// so that DisconnectSignal can clean up the callback registry.
123func SaveHandlerMapping(handlerID uint, cbPtr uintptr) {
124 if handlerID == 0 {
125 return
126 }
127
128 callbacks.Lock()
129 defer callbacks.Unlock()
130 if prevCbPtr, ok := callbacks.handlerToCallback[handlerID]; ok {
131 if prevCbPtr == cbPtr {
132 return
133 }
134 releaseCallbackRef(prevCbPtr)
135 if !hasCallbackMappings(prevCbPtr) {
136 releaseCallbackRef(prevCbPtr)
137 }
138 }
139 callbacks.handlerToCallback[handlerID] = cbPtr
140 acquireCallbackRef(cbPtr)
141}
142
143// RemoveCallbackByHandler removes a callback from the registry using a signal handler ID.
144func RemoveCallbackByHandler(handlerID uint) {
145 callbacks.Lock()
146 if cbPtr, ok := callbacks.handlerToCallback[handlerID]; ok {
147 delete(callbacks.handlerToCallback, handlerID)
148 releaseCallbackRef(cbPtr)
149 if !hasCallbackMappings(cbPtr) {
150 releaseCallbackRef(cbPtr)
151 }
152 }
153 callbacks.Unlock()
154}
155
156// SaveSourceMapping records a source ID -> callback pointer mapping.
157func SaveSourceMapping(sourceID uint, cbPtr uintptr) {
158 if sourceID == 0 {
159 return
160 }
161
162 callbacks.Lock()
163 defer callbacks.Unlock()
164 if prevCbPtr, ok := callbacks.sourceToCallback[sourceID]; ok {
165 if prevCbPtr == cbPtr {
166 return
167 }
168 releaseCallbackRef(prevCbPtr)
169 if !hasCallbackMappings(prevCbPtr) {
170 releaseCallbackRef(prevCbPtr)
171 }
172 }
173 callbacks.sourceToCallback[sourceID] = cbPtr
174 acquireCallbackRef(cbPtr)
175}
176
177// RemoveCallbackBySource removes a callback mapping using a source ID.
178func RemoveCallbackBySource(sourceID uint) {
179 callbacks.Lock()
180 if cbPtr, ok := callbacks.sourceToCallback[sourceID]; ok {
181 delete(callbacks.sourceToCallback, sourceID)
182 releaseCallbackRef(cbPtr)
183 if !hasCallbackMappings(cbPtr) {
184 releaseCallbackRef(cbPtr)
185 }
186 }
187 callbacks.Unlock()
188}
189
190// ---------------------------------------------------------------------------
191// Source callback trampoline
192//
193// GLib source functions (IdleAdd, TimeoutAdd, etc.) previously allocated a new
194// purego callback slot for every invocation. purego has a hard limit of 2000
195// slots, so long-running programs that schedule many one-shot idle/timeout
196// callbacks would exhaust the pool and panic.
197//
198// The trampoline uses a single purego callback that dispatches through a
199// Go-side map keyed by an opaque ID passed as GLib's user_data pointer.
200// This means all IdleAdd/TimeoutAdd calls share ONE purego slot regardless
201// of how many are outstanding.
202// ---------------------------------------------------------------------------
203
204// sourceEntry holds a registered GLib source callback.
205type sourceEntry struct {
206 fn SourceFunc
207 once bool // if true, automatically remove after first call (SourceOnceFunc semantics)
208}
209
210var sourceTrampolines = struct {
211 sync.Mutex
212 nextID uintptr
213 funcs map[uintptr]*sourceEntry
214 sourceToDataID map[uint]uintptr // GLib source ID → trampoline data ID
215}{
216 funcs: make(map[uintptr]*sourceEntry),
217 sourceToDataID: make(map[uint]uintptr),
218}
219
220// sourceTrampolineCb is the single purego callback shared by all source functions.
221// GLib calls it with the user_data we provided (our map key).
222var sourceTrampolineCb uintptr
223
224// sourceTrampolineOnceCb is the single purego callback for SourceOnceFunc sources
225// (IdleAddOnce, TimeoutAddOnce). These have signature func(uintptr) with no return.
226var sourceTrampolineOnceCb uintptr
227
228func initSourceTrampoline() {
229 fn := func(id uintptr) uintptr {
230 sourceTrampolines.Lock()
231 entry, ok := sourceTrampolines.funcs[id]
232 if !ok {
233 sourceTrampolines.Unlock()
234 return 0 // SOURCE_REMOVE — entry already cleaned up (e.g. by SourceRemove)
235 }
236 cb := entry.fn
237 sourceTrampolines.Unlock()
238
239 result := cb(0)
240
241 if !result {
242 sourceTrampolines.Lock()
243 delete(sourceTrampolines.funcs, id)
244 // Also clean up the reverse mapping (source ID → data ID).
245 for sid, did := range sourceTrampolines.sourceToDataID {
246 if did == id {
247 delete(sourceTrampolines.sourceToDataID, sid)
248 break
249 }
250 }
251 sourceTrampolines.Unlock()
252 }
253 if result {
254 return 1
255 }
256 return 0
257 }
258 sourceTrampolineCb = purego.NewCallback(fn)
259
260 onceFn := func(id uintptr) {
261 sourceTrampolines.Lock()
262 entry, ok := sourceTrampolines.funcs[id]
263 if !ok {
264 sourceTrampolines.Unlock()
265 return
266 }
267 cb := entry.fn
268 delete(sourceTrampolines.funcs, id)
269 // Also clean up the reverse mapping.
270 for sid, did := range sourceTrampolines.sourceToDataID {
271 if did == id {
272 delete(sourceTrampolines.sourceToDataID, sid)
273 break
274 }
275 }
276 sourceTrampolines.Unlock()
277
278 cb(0)
279 }
280 sourceTrampolineOnceCb = purego.NewCallback(onceFn)
281}
282
283// registerSourceFunc stores a SourceFunc in the trampoline map and returns
284// the trampoline callback pointer and the user_data key.
285func registerSourceFunc(fn *SourceFunc, once bool) (trampolineCb uintptr, userData uintptr) {
286 if fn == nil {
287 return 0, 0
288 }
289 sourceTrampolines.Lock()
290 sourceTrampolines.nextID++
291 id := sourceTrampolines.nextID
292 sourceTrampolines.funcs[id] = &sourceEntry{fn: *fn, once: once}
293 sourceTrampolines.Unlock()
294 if once {
295 return sourceTrampolineOnceCb, id
296 }
297 return sourceTrampolineCb, id
298}
299
300// registerSourceOnceFunc stores a SourceOnceFunc in the trampoline map and
301// returns the trampoline callback pointer and the user_data key.
302func registerSourceOnceFunc(fn *SourceOnceFunc) (trampolineCb uintptr, userData uintptr) {
303 if fn == nil {
304 return 0, 0
305 }
306 // Wrap SourceOnceFunc as SourceFunc so the entry type is uniform.
307 wrapped := SourceFunc(func(data uintptr) bool {
308 (*fn)(data)
309 return false
310 })
311 return registerSourceFunc(&wrapped, true)
312}
313
314// saveSourceTrampolineMapping records the GLib source ID → trampoline data ID
315// mapping so that SourceRemove can clean up the trampoline entry.
316func saveSourceTrampolineMapping(sourceID uint, dataID uintptr) {
317 if sourceID == 0 {
318 return
319 }
320 sourceTrampolines.Lock()
321 sourceTrampolines.sourceToDataID[sourceID] = dataID
322 sourceTrampolines.Unlock()
323}
324
325// removeSourceTrampolineBySourceID cleans up trampoline state when a GLib
326// source is removed via SourceRemove (before the callback fires).
327func removeSourceTrampolineBySourceID(sourceID uint) {
328 sourceTrampolines.Lock()
329 if dataID, ok := sourceTrampolines.sourceToDataID[sourceID]; ok {
330 delete(sourceTrampolines.sourceToDataID, sourceID)
331 delete(sourceTrampolines.funcs, dataID)
332 }
333 sourceTrampolines.Unlock()
334}
335
336// UnrefCallback removes puregotk's bookkeeping for a callback created from
337// a function pointer.
338//
339// NOTE: Upstream purego does not support releasing callback slots after
340// purego.NewCallback, so this only clears puregotk's local registry state.
341func UnrefCallback(fnPtr interface{}) error {
342 return unrefCallback(fnPtr)
343}
344
345// NewCallback converts a pointer-to-function into a C callback pointer.
346// It reuses an existing callback for the same function pointer via the
347// local callback registry.
348func NewCallback(fnPtr interface{}) uintptr {
349 val := reflect.ValueOf(fnPtr)
350 if val.IsNil() {
351 panic("purego: function must not be nil")
352 }
353 if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
354 panic("purego: the type must be a function pointer but was not")
355 }
356
357 cbPtr := val.Pointer()
358 if refPtr, ok := GetCallback(cbPtr); ok {
359 return refPtr
360 }
361
362 refPtr := purego.NewCallback(val.Elem().Interface())
363 SaveCallbackWithClosure(cbPtr, refPtr, fnPtr)
364 return refPtr
365}
366
367// NewCallbackNullable is an alias to purego.NewCallback that returns a null pointer for null functions
368func NewCallbackNullable(fn interface{}) uintptr {
369 val := reflect.ValueOf(fn)
370 if val.IsNil() {
371 return 0
372 }
373
374 return NewCallback(fn)
375}
376
377func init() {
378 initSourceTrampoline()
379}
380
381func (e *Error) Error() string {
382 return fmt.Sprintf("Gtk reported an error with message: '%s', domain: '%v' and code: '%v'", e.MessageGo(), e.Domain, e.Code)
383}
384
385func (e *Error) MessageGo() string {
386 return core.GoString(e.Message)
387}