fix(ticks): unref raw callback slots exactly once
- commit
- 3df818c98e7cb92e9a0b64f6914535a73b078fb6
- author
- brice · · 2026-07-14 07:10 UTC
- committer
- brice · · 2026-07-14 09:32 UTC
- parent
- 7b6d57c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +26 -6
M internal/infrastructure/cef/webview.go +16 -5
| @@ -39,6 +39,10 @@ var ( | |||
| 39 | 39 | // errDestroyed is returned when an operation is attempted on a destroyed WebView. | |
| 40 | 40 | var errDestroyed = errors.New("cef: webview is destroyed") | |
| 41 | 41 | ||
| 42 | + | // unrefTickCallback is the canonical puregotk/purego callback-slot release. | |
| 43 | + | // It remains injectable so lifecycle tests can prove exactly-once ownership. | |
| 44 | + | var unrefTickCallback = glib.UnrefCallback | |
| 45 | + | ||
| 42 | 46 | // errNoBrowser is returned when the browser has not been created yet. | |
| 43 | 47 | var errNoBrowser = errors.New("cef: browser not yet created") | |
| 44 | 48 | ||
| @@ -2103,26 +2107,33 @@ func (wv *WebView) startBeginFrameLoop() { | |||
| 2103 | 2107 | func (wv *WebView) stopBeginFrameLoop() { | |
| 2104 | 2108 | wv.mu.Lock() | |
| 2105 | 2109 | tickID := wv.beginFrameTickID | |
| 2110 | + | callback := wv.beginFrameTick | |
| 2106 | 2111 | wv.beginFrameTickID = 0 | |
| 2107 | 2112 | wv.beginFrameTick = nil | |
| 2108 | 2113 | bridge := wv.viewBridge | |
| 2109 | 2114 | wv.mu.Unlock() | |
| 2110 | 2115 | ||
| 2111 | - | if tickID == 0 || bridge == nil { | |
| 2112 | - | return | |
| 2116 | + | if tickID != 0 && bridge != nil { | |
| 2117 | + | if widget := bridge.Widget(); widget != nil { | |
| 2118 | + | widget.RemoveTickCallback(tickID) | |
| 2119 | + | } | |
| 2113 | 2120 | } | |
| 2114 | - | if widget := bridge.Widget(); widget != nil { | |
| 2115 | - | widget.RemoveTickCallback(tickID) | |
| 2121 | + | if callback != nil { | |
| 2122 | + | _ = unrefTickCallback(callback) | |
| 2116 | 2123 | } | |
| 2117 | 2124 | } | |
| 2118 | 2125 | ||
| 2119 | - | // releaseBeginFrameTickCallback releases the Go callback reference after GTK | |
| 2126 | + | // releaseBeginFrameTickCallback releases the purego callback slot after GTK | |
| 2120 | 2127 | // removes a source because its callback returned false. | |
| 2121 | 2128 | func (wv *WebView) releaseBeginFrameTickCallback() { | |
| 2122 | 2129 | wv.mu.Lock() | |
| 2130 | + | callback := wv.beginFrameTick | |
| 2123 | 2131 | wv.beginFrameTickID = 0 | |
| 2124 | 2132 | wv.beginFrameTick = nil | |
| 2125 | 2133 | wv.mu.Unlock() | |
| 2134 | + | if callback != nil { | |
| 2135 | + | _ = unrefTickCallback(callback) | |
| 2136 | + | } | |
| 2126 | 2137 | } | |
| 2127 | 2138 | ||
| 2128 | 2139 | func (wv *WebView) runOnGTK(fn func()) { | |
| @@ -39,6 +39,10 @@ var ( | |||||
| 39 | // errDestroyed is returned when an operation is attempted on a destroyed WebView. | 39 | // errDestroyed is returned when an operation is attempted on a destroyed WebView. | ||
| 40 | var errDestroyed = errors.New("cef: webview is destroyed") | 40 | var errDestroyed = errors.New("cef: webview is destroyed") | ||
| 41 | 41 | ||||
| 42 | + | // unrefTickCallback is the canonical puregotk/purego callback-slot release. | |||
| 43 | + | // It remains injectable so lifecycle tests can prove exactly-once ownership. | |||
| 44 | + | var unrefTickCallback = glib.UnrefCallback | |||
| 45 | + | ||||
| 42 | // errNoBrowser is returned when the browser has not been created yet. | 46 | // errNoBrowser is returned when the browser has not been created yet. | ||
| 43 | var errNoBrowser = errors.New("cef: browser not yet created") | 47 | var errNoBrowser = errors.New("cef: browser not yet created") | ||
| 44 | 48 | ||||
| @@ -2103,26 +2107,33 @@ func (wv *WebView) startBeginFrameLoop() { | |||||
| 2103 | func (wv *WebView) stopBeginFrameLoop() { | 2107 | func (wv *WebView) stopBeginFrameLoop() { | ||
| 2104 | wv.mu.Lock() | 2108 | wv.mu.Lock() | ||
| 2105 | tickID := wv.beginFrameTickID | 2109 | tickID := wv.beginFrameTickID | ||
| 2110 | + | callback := wv.beginFrameTick | |||
| 2106 | wv.beginFrameTickID = 0 | 2111 | wv.beginFrameTickID = 0 | ||
| 2107 | wv.beginFrameTick = nil | 2112 | wv.beginFrameTick = nil | ||
| 2108 | bridge := wv.viewBridge | 2113 | bridge := wv.viewBridge | ||
| 2109 | wv.mu.Unlock() | 2114 | wv.mu.Unlock() | ||
| 2110 | 2115 | ||||
| 2111 | - | if tickID == 0 || bridge == nil { | 2116 | + | if tickID != 0 && bridge != nil { |
| 2112 | - | return | 2117 | + | if widget := bridge.Widget(); widget != nil { |
| 2118 | + | widget.RemoveTickCallback(tickID) | |||
| 2119 | + | } | |||
| 2113 | } | 2120 | } | ||
| 2114 | - | if widget := bridge.Widget(); widget != nil { | 2121 | + | if callback != nil { |
| 2115 | - | widget.RemoveTickCallback(tickID) | 2122 | + | _ = unrefTickCallback(callback) |
| 2116 | } | 2123 | } | ||
| 2117 | } | 2124 | } | ||
| 2118 | 2125 | ||||
| 2119 | - | // releaseBeginFrameTickCallback releases the Go callback reference after GTK | 2126 | + | // releaseBeginFrameTickCallback releases the purego callback slot after GTK |
| 2120 | // removes a source because its callback returned false. | 2127 | // removes a source because its callback returned false. | ||
| 2121 | func (wv *WebView) releaseBeginFrameTickCallback() { | 2128 | func (wv *WebView) releaseBeginFrameTickCallback() { | ||
| 2122 | wv.mu.Lock() | 2129 | wv.mu.Lock() | ||
| 2130 | + | callback := wv.beginFrameTick | |||
| 2123 | wv.beginFrameTickID = 0 | 2131 | wv.beginFrameTickID = 0 | ||
| 2124 | wv.beginFrameTick = nil | 2132 | wv.beginFrameTick = nil | ||
| 2125 | wv.mu.Unlock() | 2133 | wv.mu.Unlock() | ||
| 2134 | + | if callback != nil { | |||
| 2135 | + | _ = unrefTickCallback(callback) | |||
| 2136 | + | } | |||
| 2126 | } | 2137 | } | ||
| 2127 | 2138 | ||||
| 2128 | func (wv *WebView) runOnGTK(fn func()) { | 2139 | func (wv *WebView) runOnGTK(fn func()) { | ||
M internal/ui/app.go +10 -1
| @@ -57,7 +57,10 @@ func gtkApplicationFlags() gio.ApplicationFlags { | |||
| 57 | 57 | return gio.GApplicationNonUniqueValue | |
| 58 | 58 | } | |
| 59 | 59 | ||
| 60 | - | var adwaitaInitOnce sync.Once | |
| 60 | + | var ( | |
| 61 | + | adwaitaInitOnce sync.Once | |
| 62 | + | unrefTickCallback = glib.UnrefCallback | |
| 63 | + | ) | |
| 61 | 64 | ||
| 62 | 65 | // EnsureAdwaitaInitialized initializes libadwaita and GTK exactly once. | |
| 63 | 66 | func EnsureAdwaitaInitialized() { | |
| @@ -4635,9 +4638,15 @@ func (session *floatingWorkspaceSession) releaseResizeTickCallback() { | |||
| 4635 | 4638 | if session == nil { | |
| 4636 | 4639 | return | |
| 4637 | 4640 | } | |
| 4641 | + | callback := session.resizeTickCallback | |
| 4638 | 4642 | session.resizeTickID = 0 | |
| 4639 | 4643 | session.resizeTickCallback = nil | |
| 4640 | 4644 | session.resizeWatcherActive = false | |
| 4645 | + | if callback != nil { | |
| 4646 | + | // AddTickCallback creates a purego function-pointer slot. Clearing the Go | |
| 4647 | + | // pointer alone cannot return that finite runtime slot to the ledger. | |
| 4648 | + | _ = unrefTickCallback(callback) | |
| 4649 | + | } | |
| 4641 | 4650 | } | |
| 4642 | 4651 | ||
| 4643 | 4652 | func (a *App) hideFloatingSession(ctx context.Context, session *floatingWorkspaceSession) { | |
| @@ -57,7 +57,10 @@ func gtkApplicationFlags() gio.ApplicationFlags { | |||||
| 57 | return gio.GApplicationNonUniqueValue | 57 | return gio.GApplicationNonUniqueValue | ||
| 58 | } | 58 | } | ||
| 59 | 59 | ||||
| 60 | - | var adwaitaInitOnce sync.Once | 60 | + | var ( |
| 61 | + | adwaitaInitOnce sync.Once | |||
| 62 | + | unrefTickCallback = glib.UnrefCallback | |||
| 63 | + | ) | |||
| 61 | 64 | ||||
| 62 | // EnsureAdwaitaInitialized initializes libadwaita and GTK exactly once. | 65 | // EnsureAdwaitaInitialized initializes libadwaita and GTK exactly once. | ||
| 63 | func EnsureAdwaitaInitialized() { | 66 | func EnsureAdwaitaInitialized() { | ||
| @@ -4635,9 +4638,15 @@ func (session *floatingWorkspaceSession) releaseResizeTickCallback() { | |||||
| 4635 | if session == nil { | 4638 | if session == nil { | ||
| 4636 | return | 4639 | return | ||
| 4637 | } | 4640 | } | ||
| 4641 | + | callback := session.resizeTickCallback | |||
| 4638 | session.resizeTickID = 0 | 4642 | session.resizeTickID = 0 | ||
| 4639 | session.resizeTickCallback = nil | 4643 | session.resizeTickCallback = nil | ||
| 4640 | session.resizeWatcherActive = false | 4644 | session.resizeWatcherActive = false | ||
| 4645 | + | if callback != nil { | |||
| 4646 | + | // AddTickCallback creates a purego function-pointer slot. Clearing the Go | |||
| 4647 | + | // pointer alone cannot return that finite runtime slot to the ledger. | |||
| 4648 | + | _ = unrefTickCallback(callback) | |||
| 4649 | + | } | |||
| 4641 | } | 4650 | } | ||
| 4642 | 4651 | ||||
| 4643 | func (a *App) hideFloatingSession(ctx context.Context, session *floatingWorkspaceSession) { | 4652 | func (a *App) hideFloatingSession(ctx context.Context, session *floatingWorkspaceSession) { | ||