Skip to content

fix(android): NaN-box UI handles, safe preferencesGet, status-bar host pad#6317

Merged
proggeramlug merged 7 commits into
PerryTS:mainfrom
tangledcircuitweb:fix/android-nanbox-callbacks-and-symbols
Jul 13, 2026
Merged

fix(android): NaN-box UI handles, safe preferencesGet, status-bar host pad#6317
proggeramlug merged 7 commits into
PerryTS:mainfrom
tangledcircuitweb:fix/android-nanbox-callbacks-and-symbols

Conversation

@tangledcircuitweb

@tangledcircuitweb tangledcircuitweb commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardening the Android UI backend so TS-side NaN-boxed handles and SharedPreferences number keys no longer crash or silently no-op. Proven on Cuttlefish (x86_64) and OnePlus (arm64) with a real stopwatch app.

These are Android-only (perry-ui-android + activity template). They align invoke/style paths with iOS/macOS unbox behavior and fix a classic typed-prefs JNI footgun.

Changes

  • Callbacks: js_nanbox_get_pointer before js_closure_call* on button/pointer/drag_drop/state/app lifecycle (match iOS/macOS). Raw to_bits() as *const u8 left the 0x7ffd tag in the pointer and corrupted the heap — UI could open, any press died on RenderThread (FORTIFY: pthread_mutex_lock called on a destroyed mutex / SIGSEGV).
  • Style FFIs: decode POINTER + INT32 NaN-box tags for widget handles (f64 handle args) so set_background_color / corner radius / etc. find widgets; MaterialButton uses backgroundTintList so solid fills actually paint.
  • Text parity: export perry_ui_text_create_with_id / perry_ui_set_text on Android for Text(id) + setText.
  • Link: -landroid so ANativeWindow_fromSurface resolves at dlopen.
  • preferencesGet: do not call getString on float-stored keys (ClassCastException left a pending JNI exception → subsequent text/hstack creates panicked and half the UI vanished). Read via getAll and branch on String/Float/Double/Integer/Long; clear leftover exceptions.
  • Activity template: stop FLAG_LAYOUT_NO_LIMITS without real safe-area insets; setDecorFitsSystemWindows + pad host FrameLayout by status_bar_height so titles are not under system icons on Android 15 / OnePlus. (getSafeAreaInsets still returns zeros on Android.)

Out of scope / not proposed as platform defaults

  • App-level hardwareAccelerated=false for Adreno CircularRRectOp aborts on rounded rects (device workaround; kept in sample app only).
  • Cuttlefish x86_64 TLS issues with setInterval (separate runtime/emulator story).

Related issue

n/a — found while dogfooding Android phone + Cuttlefish. Happy to file issues if you prefer them linked after the fact.

Test plan

  • Rebuild perry-ui-android (+ runtime/stdlib as needed) for x86_64-linux-android and aarch64-linux-android with global-dynamic TLS
  • Cuttlefish (adb wireless, x86_64 APK): app opens; Start/Pause/Reset; logcat shows clean invoke0 without high 0x7ffd ptr tag; no RenderThread crash
  • OnePlus arm64 release APK: same UI + timer; colors paint; prefs restore no longer wipes title/clock/buttons; content below status bar
  • After numeric preferencesSet + relaunch: full UI still builds (no pending ClassCastException)
  • CI on this PR
  • cargo build --release clean (contributor machine; CI will re-check)
  • Workspace tests excluding platform UI crates (CI)

Screenshots / output

Phone (arm64): DaisyUI-styled stopwatch with live clock, status-bar clearance, Start → Running with advancing elapsed. Prefs restore of elapsed no longer blanks half the tree.

Checklist

  • I have NOT bumped the workspace version or edited CLAUDE.md / CHANGELOG.md (maintainer handles these at merge)
  • Commits use fix(android): conventional-style prefixes
  • I've read CONTRIBUTING.md and agree to the Code of Conduct

Summary by CodeRabbit

  • New Features

    • Added reactive text creation and updates using text identifiers.
    • Improved Android widget styling, sizing, colors, borders, shadows, opacity, and text appearance handling.
    • Added safer Android window inset and system bar handling across supported versions.
  • Bug Fixes

    • Fixed callback handling to prevent crashes caused by tagged pointer values.
    • Improved preference retrieval across different Android value types.
    • Corrected widget background colors and button tint behavior.
    • Improved drag-and-drop and lifecycle callback reliability.

…k symbols

Button presses crashed after UI open (RenderThread destroyed-mutex / SIGSEGV)
because invoke* cast NaN-box bits to pointers. Match iOS/macOS: use
js_nanbox_get_pointer in callback, pointer, drag_drop, state onChange, and
app activate/terminate paths. Leave render_for_each raw bitcast (documented
non-nanbox).

Also export perry_ui_text_create_with_id / perry_ui_set_text on Android (parity
with other platforms for Text(id)/setText), and link -landroid so
ANativeWindow_fromSurface resolves at dlopen.

Verified on Cuttlefish x86_64 (Start/Pause/Reset stopwatch) and phone arm64.
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 716593ed-e43c-4e6f-a220-48030ce3ae5f

📥 Commits

Reviewing files that changed from the base of the PR and between 6cbe540 and dde8f1d.

📒 Files selected for processing (2)
  • crates/perry-ui-android/src/ffi/basic_widgets.rs
  • crates/perry-ui-android/template/app/src/main/java/com/perry/app/PerryActivity.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-ui-android/src/ffi/basic_widgets.rs

📝 Walkthrough

Walkthrough

Android callback paths now unbox NaN-boxed closure pointers through a native helper. Widget FFI handles use JavaScript-compatible floating-point decoding, preference reads handle multiple Java types, and Android text, styling, window, and linker behavior are updated.

Changes

Android runtime and UI integration

Layer / File(s) Summary
Replace tagged pointer extraction
crates/perry-ui-android/src/app.rs, crates/perry-ui-android/src/callback.rs, crates/perry-ui-android/src/drag_drop.rs, crates/perry-ui-android/src/pointer.rs, crates/perry-ui-android/src/state.rs
Callback paths use js_nanbox_get_pointer instead of converting NaN-boxed closures with to_bits().
Decode JavaScript widget handles
crates/perry-ui-android/src/widgets/mod.rs, crates/perry-ui-android/src/ffi/image_nav.rs, crates/perry-ui-android/src/ffi/tabbar_layout.rs, crates/perry-ui-android/src/ffi/text_scroll.rs
Widget handles are accepted as f64, decoded through decode_js_handle_f64, and validated before widget operations.
Harden preference retrieval
crates/perry-ui-android/src/system.rs
Preference lookup uses getAll() and runtime Java type checks, with JNI exception cleanup and 0.0 fallback handling.
Update Android UI behavior
crates/perry-ui-android/src/ffi/basic_widgets.rs, crates/perry-ui-android/src/widgets/mod.rs
Text creation and updates are exported, and background styling updates drawable and tint handling.
Update window and linker setup
crates/perry-ui-android/template/app/src/main/java/com/perry/app/PerryActivity.kt, crates/perry/src/commands/compile/link/build_and_run.rs
Window inset handling and Android linking are updated, including the -landroid library.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • PerryTS/perry#5513: Both changes replace unsafe interpretation of NaN-boxed closure values before native closure invocation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main Android fixes: NaN-boxed handles, safer preferences retrieval, and status-bar layout.
Description check ✅ Passed The description matches the template well, with summary, changes, related issue, test plan, screenshots, and checklist sections filled in.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
crates/perry-ui-android/src/callback.rs (1)

139-139: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding the tag-bit warning comment to the other invoke variants.

invoke0 has an excellent three-line comment explaining why to_bits() leaves the 0x7ffd tag and crashes. The other variants (invoke1, invoke2, invoke4, invoke_with_string_array) perform the same unboxing without any comment, making it less obvious to future maintainers why js_nanbox_get_pointer is required here.

A brief one-line comment (e.g. // Unbox NaN-boxed closure — see invoke0 for details.) in each variant would prevent someone from "simplifying" back to to_bits().

Also applies to: 153-153, 168-168, 185-185

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-ui-android/src/callback.rs` at line 139, Add a brief explanatory
comment before the js_nanbox_get_pointer call in invoke1, invoke2, invoke4, and
invoke_with_string_array, matching invoke0’s guidance that NaN-boxed closures
must be unboxed through this helper rather than to_bits(). Keep the existing
unboxing logic unchanged.
crates/perry-ui-android/src/app.rs (1)

362-362: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

NaN-box unboxing in lifecycle handlers looks correct.

The switch from to_bits() to js_nanbox_get_pointer() in handle_activate and handle_terminate is consistent with the pattern used in callback.rs, pointer.rs, drag_drop.rs, and state.rs. The FFI signature fn js_nanbox_get_pointer(value: f64) -> i64 matches the declaration in the other files.

One minor inconsistency: unlike pointer.rs and drag_drop.rs, these handlers don't null-check ptr after unboxing before calling js_closure_call1. Since the callback was stored from a valid f64, a null return would indicate a nanbox helper bug rather than a missing callback — but adding a guard would make the defensive posture uniform across all callback paths.

Also applies to: 410-411, 423-423

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-ui-android/src/app.rs` at line 362, In the lifecycle handlers
handle_activate and handle_terminate, add the same null-pointer guard used by
pointer.rs and drag_drop.rs after js_nanbox_get_pointer() unboxes the callback
value, before invoking js_closure_call1; preserve the existing callback behavior
for valid pointers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry-ui-android/src/app.rs`:
- Line 362: In the lifecycle handlers handle_activate and handle_terminate, add
the same null-pointer guard used by pointer.rs and drag_drop.rs after
js_nanbox_get_pointer() unboxes the callback value, before invoking
js_closure_call1; preserve the existing callback behavior for valid pointers.

In `@crates/perry-ui-android/src/callback.rs`:
- Line 139: Add a brief explanatory comment before the js_nanbox_get_pointer
call in invoke1, invoke2, invoke4, and invoke_with_string_array, matching
invoke0’s guidance that NaN-boxed closures must be unboxed through this helper
rather than to_bits(). Keep the existing unboxing logic unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c59920ec-6f59-4e12-8742-6f5b530c447d

📥 Commits

Reviewing files that changed from the base of the PR and between 0960415 and 5b4cc6f.

📒 Files selected for processing (7)
  • crates/perry-ui-android/src/app.rs
  • crates/perry-ui-android/src/callback.rs
  • crates/perry-ui-android/src/drag_drop.rs
  • crates/perry-ui-android/src/ffi/basic_widgets.rs
  • crates/perry-ui-android/src/pointer.rs
  • crates/perry-ui-android/src/state.rs
  • crates/perry/src/commands/compile/link/build_and_run.rs

Add unbox comments on remaining invoke* helpers and null-check
lifecycle callback pointers after js_nanbox_get_pointer.
declare-function styling calls pass POINTER-tagged f64 handles
(0x7FFD…payload). i64 parameters left the real handle in xmm while C
read rdi garbage, so set_background_color never found widgets.

- decode_js_handle_f64 for POINTER + INT32 tags
- style FFIs take handle: f64
- MaterialButton backgroundTintList so fills actually show
preferencesGet used getString first on SharedPreferences. When a key was
stored as float (preferencesSet with a number), Android threw
ClassCastException and left a pending JNI exception, which poisoned the
rest of nativeMain (text/hstack create panics, half the UI missing).

Read via getAll and branch on String/Float/Double/Integer/Long, clearing
any leftover exception before return.

Also stop the activity template from using FLAG_LAYOUT_NO_LIMITS without
real safe-area insets: pad the host FrameLayout by status_bar_height so
content is not under system icons on Android 15 / OnePlus.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@crates/perry-ui-android/template/app/src/main/java/com/perry/app/PerryActivity.kt`:
- Around line 41-62: Make the status bar padding in PerryActivity conditional on
the window layout mode: when setDecorFitsSystemWindows(true) is used on API
30–34, do not apply statusPad because system insets already position the content
below the bar; only retain the padding for the edge-to-edge path, or remove it
if decor-fits behavior is intended. Update the rootLayout.setPadding call and
the surrounding decor configuration consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 31c1f38a-f7db-4610-8271-c141dcf8b935

📥 Commits

Reviewing files that changed from the base of the PR and between 15f113a and 6cbe540.

📒 Files selected for processing (6)
  • crates/perry-ui-android/src/ffi/image_nav.rs
  • crates/perry-ui-android/src/ffi/tabbar_layout.rs
  • crates/perry-ui-android/src/ffi/text_scroll.rs
  • crates/perry-ui-android/src/system.rs
  • crates/perry-ui-android/src/widgets/mod.rs
  • crates/perry-ui-android/template/app/src/main/java/com/perry/app/PerryActivity.kt

Comment thread crates/perry-ui-android/template/app/src/main/java/com/perry/app/PerryActivity.kt Outdated
@tangledcircuitweb tangledcircuitweb changed the title fix(android): unbox NaN-boxed UI callbacks + missing text/android link symbols fix(android): NaN-box UI handles, safe preferencesGet, status-bar host pad Jul 12, 2026
@proggeramlug

Copy link
Copy Markdown
Contributor

thanks @tangledcircuitweb I'm quickly fixing lint + the minor finding by coderabbit and then I'll merge :)

Great addition!

Two follow-ups on the review:

- `cargo fmt` drift in `ffi/basic_widgets.rs` (the two new text-registry
  call sites) — this is what the `lint` gate was failing on.

- The host `FrameLayout` padded unconditionally by `status_bar_height`.
  That is only correct in the edge-to-edge regime. The template targets
  SDK 35, so on Android 15+ `setDecorFitsSystemWindows(true)` is a no-op
  and nothing insets the content — the pad is needed. But on API 30-34
  that call is honored (and below 30 the decor fits by default), so the
  content view is already inset and the extra pad is just blank space at
  the top.

  Pad from the insets actually dispatched to the host instead of
  branching on SDK_INT: the decor consumes them when it fits system
  windows (so we pad by zero) and passes them through when it does not.
  Also picks up the nav bar, display cutouts and landscape, which a
  top-only status_bar_height pad missed.

Verified by building the rendered template with AGP 8.8.2 / SDK 35.
@proggeramlug proggeramlug merged commit cb750dd into PerryTS:main Jul 13, 2026
24 checks passed
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.

2 participants