Skip to content

fix(ui): root persistent JavaScript callbacks - #8713

Closed
proggeramlug wants to merge 2 commits into
PerryTS:mainfrom
proggeramlug:fix/8701-ui-callback-roots
Closed

fix(ui): root persistent JavaScript callbacks#8713
proggeramlug wants to merge 2 commits into
PerryTS:mainfrom
proggeramlug:fix/8701-ui-callback-roots

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes #8701

Summary

  • register one mutable GC root scanner for the shared dispatcher and every platform UI crate, visiting and rewriting every persisted JavaScript callback and state value
  • keep GTK timers and macOS native blocks behind rewriteable keyed tables, cover the shared miniaudio callbacks, and add raw const-pointer slot visitation for Windows callbacks
  • graduate all 474 UI census candidates: 265 are scanner-covered and 209 are audited non-GC holders; strengthen the census call-graph and promotion regressions

Validation

  • python3 scripts/gc_runtime_root_holders.py --self-test
  • python3 scripts/gc_runtime_root_holders.py (zero UI frontier entries)
  • cargo test -p perry-ffi -- --test-threads=1
  • cargo check -p perry-ffi -p perry-ui -p perry-ui-macos -p perry-audio-miniaudio
  • cargo check -p perry-ui-ios --target aarch64-apple-ios
  • cargo check -p perry-ui-tvos --target aarch64-apple-tvos
  • cargo check -p perry-ui-visionos --target aarch64-apple-visionos
  • cargo check -p perry-ui-watchos --target aarch64-apple-watchos
  • Android and Windows crates type-check for their cross targets with only the unavailable native miniaudio C compilation skipped
  • GTK cross-check is blocked at the host dependency boundary because GStreamer development metadata is not installed

No package version bump.

Summary by CodeRabbit

  • Bug Fixes
    • Improved UI stability by retaining JavaScript callbacks and state values across garbage collection on all native UI platforms.
    • Preserved callbacks used by timers, keyboard shortcuts, focus events, media playback, notifications, gestures, and UI widgets.
  • Documentation
    • Added changelog coverage for the garbage-collection reliability improvements.
  • Tests
    • Expanded validation for callback retention and garbage-collection behavior.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 18 minutes.

View limit details

Limit details: You’ve used all 8 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c4c4c32e-f993-4194-9f5d-6cb7e49dbda0

📥 Commits

Reviewing files that changed from the base of the PR and between 60f4fab and 31ecbf1.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (197)
  • changelog.d/8713-ui-callback-gc-roots.md
  • crates/perry-audio-miniaudio/src/lib.rs
  • crates/perry-ffi/src/handle.rs
  • crates/perry-ui-android/src/app.rs
  • crates/perry-ui-android/src/callback.rs
  • crates/perry-ui-android/src/gc.rs
  • crates/perry-ui-android/src/lib.rs
  • crates/perry-ui-android/src/media_playback.rs
  • crates/perry-ui-android/src/state.rs
  • crates/perry-ui-android/src/widgets/lazyvstack.rs
  • crates/perry-ui-android/src/widgets/picker.rs
  • crates/perry-ui-android/src/widgets/webview.rs
  • crates/perry-ui-android/src/ws.rs
  • crates/perry-ui-gtk4/src/app.rs
  • crates/perry-ui-gtk4/src/audio.rs
  • crates/perry-ui-gtk4/src/camera.rs
  • crates/perry-ui-gtk4/src/drag_drop.rs
  • crates/perry-ui-gtk4/src/ffi/platform_audio_camera_toast.rs
  • crates/perry-ui-gtk4/src/gc.rs
  • crates/perry-ui-gtk4/src/lib.rs
  • crates/perry-ui-gtk4/src/media_playback.rs
  • crates/perry-ui-gtk4/src/menu.rs
  • crates/perry-ui-gtk4/src/state.rs
  • crates/perry-ui-gtk4/src/toolbar.rs
  • crates/perry-ui-gtk4/src/tray.rs
  • crates/perry-ui-gtk4/src/widgets/bottom_nav.rs
  • crates/perry-ui-gtk4/src/widgets/button.rs
  • crates/perry-ui-gtk4/src/widgets/command_palette.rs
  • crates/perry-ui-gtk4/src/widgets/image_gallery.rs
  • crates/perry-ui-gtk4/src/widgets/lazyvstack.rs
  • crates/perry-ui-gtk4/src/widgets/picker.rs
  • crates/perry-ui-gtk4/src/widgets/scrollview.rs
  • crates/perry-ui-gtk4/src/widgets/securefield.rs
  • crates/perry-ui-gtk4/src/widgets/slider.rs
  • crates/perry-ui-gtk4/src/widgets/textarea.rs
  • crates/perry-ui-gtk4/src/widgets/textfield.rs
  • crates/perry-ui-gtk4/src/widgets/toggle.rs
  • crates/perry-ui-gtk4/src/widgets/webview.rs
  • crates/perry-ui-ios/src/adaptive_layout.rs
  • crates/perry-ui-ios/src/app.rs
  • crates/perry-ui-ios/src/audio_playback.rs
  • crates/perry-ui-ios/src/background.rs
  • crates/perry-ui-ios/src/camera.rs
  • crates/perry-ui-ios/src/deeplinks.rs
  • crates/perry-ui-ios/src/drag_drop.rs
  • crates/perry-ui-ios/src/gc.rs
  • crates/perry-ui-ios/src/geolocation.rs
  • crates/perry-ui-ios/src/lib.rs
  • crates/perry-ui-ios/src/location.rs
  • crates/perry-ui-ios/src/media_playback.rs
  • crates/perry-ui-ios/src/menu.rs
  • crates/perry-ui-ios/src/network.rs
  • crates/perry-ui-ios/src/notifications.rs
  • crates/perry-ui-ios/src/pointer.rs
  • crates/perry-ui-ios/src/state.rs
  • crates/perry-ui-ios/src/widgets/bottom_nav.rs
  • crates/perry-ui-ios/src/widgets/button.rs
  • crates/perry-ui-ios/src/widgets/calendar.rs
  • crates/perry-ui-ios/src/widgets/combobox.rs
  • crates/perry-ui-ios/src/widgets/date_picker.rs
  • crates/perry-ui-ios/src/widgets/image_gallery.rs
  • crates/perry-ui-ios/src/widgets/mod.rs
  • crates/perry-ui-ios/src/widgets/picker.rs
  • crates/perry-ui-ios/src/widgets/rich_text.rs
  • crates/perry-ui-ios/src/widgets/scrollview.rs
  • crates/perry-ui-ios/src/widgets/securefield.rs
  • crates/perry-ui-ios/src/widgets/slider.rs
  • crates/perry-ui-ios/src/widgets/tabbar.rs
  • crates/perry-ui-ios/src/widgets/textarea.rs
  • crates/perry-ui-ios/src/widgets/textfield.rs
  • crates/perry-ui-ios/src/widgets/toggle.rs
  • crates/perry-ui-ios/src/widgets/tree_view.rs
  • crates/perry-ui-ios/src/widgets/webview.rs
  • crates/perry-ui-ios/src/widgets/wheel_picker.rs
  • crates/perry-ui-macos/src/app.rs
  • crates/perry-ui-macos/src/audio_playback.rs
  • crates/perry-ui-macos/src/background.rs
  • crates/perry-ui-macos/src/deeplinks.rs
  • crates/perry-ui-macos/src/drag_drop.rs
  • crates/perry-ui-macos/src/gc.rs
  • crates/perry-ui-macos/src/geolocation.rs
  • crates/perry-ui-macos/src/lib.rs
  • crates/perry-ui-macos/src/location.rs
  • crates/perry-ui-macos/src/media_playback.rs
  • crates/perry-ui-macos/src/menu.rs
  • crates/perry-ui-macos/src/network.rs
  • crates/perry-ui-macos/src/notifications.rs
  • crates/perry-ui-macos/src/pointer.rs
  • crates/perry-ui-macos/src/state.rs
  • crates/perry-ui-macos/src/tray.rs
  • crates/perry-ui-macos/src/widgets/bottom_nav.rs
  • crates/perry-ui-macos/src/widgets/button.rs
  • crates/perry-ui-macos/src/widgets/calendar.rs
  • crates/perry-ui-macos/src/widgets/combobox.rs
  • crates/perry-ui-macos/src/widgets/command_palette.rs
  • crates/perry-ui-macos/src/widgets/date_picker.rs
  • crates/perry-ui-macos/src/widgets/image_gallery.rs
  • crates/perry-ui-macos/src/widgets/lazyvstack.rs
  • crates/perry-ui-macos/src/widgets/mod.rs
  • crates/perry-ui-macos/src/widgets/picker.rs
  • crates/perry-ui-macos/src/widgets/rich_text.rs
  • crates/perry-ui-macos/src/widgets/scrollview.rs
  • crates/perry-ui-macos/src/widgets/securefield.rs
  • crates/perry-ui-macos/src/widgets/slider.rs
  • crates/perry-ui-macos/src/widgets/table.rs
  • crates/perry-ui-macos/src/widgets/textarea.rs
  • crates/perry-ui-macos/src/widgets/textfield.rs
  • crates/perry-ui-macos/src/widgets/toggle.rs
  • crates/perry-ui-macos/src/widgets/toolbar.rs
  • crates/perry-ui-macos/src/widgets/tree_view.rs
  • crates/perry-ui-macos/src/widgets/webview.rs
  • crates/perry-ui-tvos/src/app.rs
  • crates/perry-ui-tvos/src/audio_playback.rs
  • crates/perry-ui-tvos/src/background.rs
  • crates/perry-ui-tvos/src/gc.rs
  • crates/perry-ui-tvos/src/lib.rs
  • crates/perry-ui-tvos/src/location.rs
  • crates/perry-ui-tvos/src/media_playback.rs
  • crates/perry-ui-tvos/src/menu.rs
  • crates/perry-ui-tvos/src/pointer.rs
  • crates/perry-ui-tvos/src/state.rs
  • crates/perry-ui-tvos/src/widgets/bottom_nav.rs
  • crates/perry-ui-tvos/src/widgets/button.rs
  • crates/perry-ui-tvos/src/widgets/mod.rs
  • crates/perry-ui-tvos/src/widgets/scrollview.rs
  • crates/perry-ui-tvos/src/widgets/securefield.rs
  • crates/perry-ui-tvos/src/widgets/slider.rs
  • crates/perry-ui-tvos/src/widgets/tabbar.rs
  • crates/perry-ui-tvos/src/widgets/textarea.rs
  • crates/perry-ui-tvos/src/widgets/textfield.rs
  • crates/perry-ui-tvos/src/widgets/toggle.rs
  • crates/perry-ui-visionos/src/app.rs
  • crates/perry-ui-visionos/src/audio_playback.rs
  • crates/perry-ui-visionos/src/background.rs
  • crates/perry-ui-visionos/src/camera.rs
  • crates/perry-ui-visionos/src/drag_drop.rs
  • crates/perry-ui-visionos/src/gc.rs
  • crates/perry-ui-visionos/src/lib.rs
  • crates/perry-ui-visionos/src/location.rs
  • crates/perry-ui-visionos/src/media_playback.rs
  • crates/perry-ui-visionos/src/menu.rs
  • crates/perry-ui-visionos/src/pointer.rs
  • crates/perry-ui-visionos/src/state.rs
  • crates/perry-ui-visionos/src/widgets/bottom_nav.rs
  • crates/perry-ui-visionos/src/widgets/button.rs
  • crates/perry-ui-visionos/src/widgets/calendar.rs
  • crates/perry-ui-visionos/src/widgets/combobox.rs
  • crates/perry-ui-visionos/src/widgets/date_picker.rs
  • crates/perry-ui-visionos/src/widgets/mod.rs
  • crates/perry-ui-visionos/src/widgets/rich_text.rs
  • crates/perry-ui-visionos/src/widgets/scrollview.rs
  • crates/perry-ui-visionos/src/widgets/securefield.rs
  • crates/perry-ui-visionos/src/widgets/slider.rs
  • crates/perry-ui-visionos/src/widgets/tabbar.rs
  • crates/perry-ui-visionos/src/widgets/textarea.rs
  • crates/perry-ui-visionos/src/widgets/textfield.rs
  • crates/perry-ui-visionos/src/widgets/tree_view.rs
  • crates/perry-ui-visionos/src/widgets/webview.rs
  • crates/perry-ui-visionos/src/widgets/wheel_picker.rs
  • crates/perry-ui-watchos/src/audio_playback.rs
  • crates/perry-ui-watchos/src/background.rs
  • crates/perry-ui-watchos/src/gc.rs
  • crates/perry-ui-watchos/src/lib.rs
  • crates/perry-ui-watchos/src/media_playback.rs
  • crates/perry-ui-watchos/src/notifications.rs
  • crates/perry-ui-watchos/src/state.rs
  • crates/perry-ui-watchos/src/tree.rs
  • crates/perry-ui-windows/src/app.rs
  • crates/perry-ui-windows/src/drag_drop.rs
  • crates/perry-ui-windows/src/gc.rs
  • crates/perry-ui-windows/src/lib.rs
  • crates/perry-ui-windows/src/media_playback.rs
  • crates/perry-ui-windows/src/menu.rs
  • crates/perry-ui-windows/src/pointer.rs
  • crates/perry-ui-windows/src/state.rs
  • crates/perry-ui-windows/src/toolbar.rs
  • crates/perry-ui-windows/src/tray.rs
  • crates/perry-ui-windows/src/widgets/bottom_nav.rs
  • crates/perry-ui-windows/src/widgets/button.rs
  • crates/perry-ui-windows/src/widgets/calendar.rs
  • crates/perry-ui-windows/src/widgets/combobox.rs
  • crates/perry-ui-windows/src/widgets/command_palette.rs
  • crates/perry-ui-windows/src/widgets/date_picker.rs
  • crates/perry-ui-windows/src/widgets/image_gallery.rs
  • crates/perry-ui-windows/src/widgets/lazyvstack.rs
  • crates/perry-ui-windows/src/widgets/rich_text.rs
  • crates/perry-ui-windows/src/widgets/scrollview.rs
  • crates/perry-ui-windows/src/widgets/slider.rs
  • crates/perry-ui-windows/src/widgets/table.rs
  • crates/perry-ui-windows/src/widgets/tree_view.rs
  • crates/perry-ui-windows/src/widgets/webview.rs
  • crates/perry-ui-windows/src/window.rs
  • crates/perry-ui/Cargo.toml
  • crates/perry-ui/src/key_dispatch.rs
  • crates/perry-ui/src/lib.rs
  • scripts/gc_runtime_root_holders.json
  • scripts/gc_runtime_root_holders.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

proggeramlug added a commit that referenced this pull request Aug 24, 2026
… layer (#8721)

Lands #8713, #8704 and #8717.

#8713 (closes #8701) fixes the systemic gap I filed while auditing #8699:
every perry-ui-* crate cached JS callbacks as raw NaN-boxed f64 in
thread-local side tables, and NO UI crate registered a root scanner. A
stored callback is a heap pointer held across collections -- unmarked, so
a live listener could be reclaimed, and unrewritten, so it could dangle
after an evacuating minor and surface cycles later as "value is not a
function".

It registers one mutable root scanner per UI crate and for the shared
dispatcher, and graduates all 474 census candidates: the frontier drops
628 -> 158 while audited holders rise 71 -> 280. The specific holder I
named in #8701, perry-ui-ios adaptive_layout LISTENERS, is now
scanner-covered; its sibling LAST_SNAPSHOT is correctly classified
not_a_gc_pointer (native viewport dimensions, no JS value).

The census got STRONGER, not weaker -- checked, because graduating 468
holders by relaxing the checker would look identical on the surface. Its
self-test goes from 89 planted declarations / 0 inventory entries checked
to 90 / 280. main's self-test validated none of the ledger; this one
validates all of it.

#8704 completes the bun:ffi / node:ffi C ABI. The raw-handle debt that
held it is resolved: bare reads are 913 against a baseline of 913, with
the baseline LOWERED from 918 -- a ratchet tightening, not a waiver.

#8717 renders Windows PdfView pages natively through Windows.Data.Pdf.

Two mechanical fixes on top: a not_a_gc_pointer verdict for #8717's
COMPLETED render table (keyed by a monotonic request id, holding only
owned PNG bytes or an error String), a changelog fragment for #8717, and
deletion of three ledger entries that go stale once #8704's scanner
widens call-graph coverage.

No version bump.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed on main via #8721 (squash 3f655301e), with #8704 and #8717. This closes #8701, which I filed while auditing #8699 — thanks for taking it on at this scale.

I checked the thing that mattered most: graduating 468 holders by relaxing the checker would look identical from the outside to graduating them by covering them. So I compared the self-test against main:

main this PR
planted declarations classified 89 90
inventory entries checked 0 280

main's self-test validated none of the ledger; yours validates all of it. That's a strengthening, and it closes the "a list nobody checks" problem the gate's own error text warns about.

Also verified the specific holder from #8701: perry-ui-ios adaptive_layout::LISTENERS is now scanner-covered, and LAST_SNAPSHOT is correctly not_a_gc_pointer — native viewport dimensions, no JS value. And ensure_registered() is genuinely reached (from app_create), not merely defined.

One observation, not a blocker: ensure_registered() is only called from app_create() (and one other site), so a callback stored before the app exists would sit unscanned — perry_ios_on_layout_change, for instance, stores into LISTENERS without registering. That can't happen in a real UI program, since you create the app first, but a Once-guarded call at each callback-storing FFI entry would close the window for free. Worth considering if you're back in this code.

Validated on the merged result: all 30 lint checkers, runtime 2661/0, codegen 1214/0, transform 92/0, perry-ui 8/0 + 2/0.

@proggeramlug
proggeramlug deleted the fix/8701-ui-callback-roots branch August 24, 2026 11:19
proggeramlug added a commit that referenced this pull request Aug 24, 2026
Lands #8722 with its four gate blockers resolved.

Replaces the windows-winui scaffold with real WinUI 3 / Fluent rendering
through a vendored Windows Reactor snapshot (microsoft/windows-rs
65066a7109c214f317ed66261cfb7518160b8aaf), upstream licenses preserved.

The vendored tree moved from `crates/perry-ui-windows-winui/vendor/` to
`third_party/windows-winui/`, and this was forced rather than chosen.
Cargo's `is_excluded` is `!explicit_member && excluded`, and
`explicit_member` matches any `members` entry that is a path PREFIX of the
candidate -- so while the snapshot sat under a member directory, `exclude`
lost and all 13 crates were real workspace members. Verified empirically:
adding `exclude = ["crates/perry-ui-windows-winui/vendor"]` in place left
cargo reporting 92 packages with all 13 still members; after the move it
reports 79, matching the existing architecture baseline exactly, so no
baseline refresh was needed. As members they entered the build graph, so
`cargo test --workspace` compiled them and Perry's `[workspace.lints]`
would have applied to upstream code.

GC roots, the part that mattered: `Node` DOES hold JS values -- every
widget callback is stored as a raw closure pointer via
`js_nanbox_get_pointer` -- so it gets scanner coverage following #8713's
pattern, not a verdict. `AppState` does not: `String`, two `f64`
dimensions, an `i64` that indexes NODES rather than an address, two
`Option<(f64,f64)>` and a `PresenterKind`, so it takes a
`not_a_gc_pointer` verdict. Three further roots the census had not
flagged are scanned too (`ON_ACTIVATE`, `ON_TERMINATE`, `PENDING_TIMERS`),
and the scanner is armed on the Fluent path, where winui shadows
`app_create` and would otherwise have left `perry-ui-windows`' own tables
unscanned.

The four oversized files are generated upstream bindings and are
allowlisted as an immutable snapshot. `build_and_run.rs` was NOT
allowlisted -- the block this PR added moved to `link/winui_assets.rs`,
2110 -> 1970.

No version bump; the only root Cargo.toml change is the `exclude` entry.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perry-ui-* callback tables hold NaN-boxed JS closures with no GC scanner

1 participant