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