Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions changelog.d/8713-ui-callback-gc-roots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

Root persistent `perry/ui` JavaScript callbacks and state values across garbage
collections on every native UI backend, including callbacks retained by native
timer, hotkey, and focus-event closures.
32 changes: 31 additions & 1 deletion crates/perry-audio-miniaudio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use libc::{c_char, c_float, c_int, c_uint, c_void};
use std::cell::RefCell;
use std::ffi::CString;
use std::sync::Mutex;
use std::sync::{Mutex, Once};

use perry_ffi::copy_string_from_raw as str_from_header;

Expand Down Expand Up @@ -227,6 +227,8 @@ struct Fade {
then_stop: bool,
}

static GC_SCANNER_REGISTERED: Once = Once::new();

thread_local! {
static ENGINE: RefCell<Option<MaBox<MA_ENGINE_SIZE>>> = RefCell::new(None);
static SOUNDS: RefCell<Vec<Option<SoundEntry>>> = RefCell::new(Vec::new());
Expand All @@ -238,6 +240,32 @@ thread_local! {
static PENDING_LOADED: RefCell<Vec<usize>> = RefCell::new(Vec::new());
}

fn ensure_gc_scanner_registered() {
GC_SCANNER_REGISTERED.call_once(|| {
perry_ffi::gc_register_mutable_root_scanner_named(
"perry-audio-miniaudio",
scan_miniaudio_gc_roots,
);
});
}

fn scan_miniaudio_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
SOUNDS.with(|sounds| {
for sound in sounds.borrow_mut().iter_mut().flatten() {
if let Some(callback) = sound.on_loaded.as_mut() {
visitor.visit_nanbox_f64_slot(callback);
}
}
});
VOICES.with(|voices| {
for voice in voices.borrow_mut().iter_mut().flatten() {
if let Some(callback) = voice.on_ended.as_mut() {
visitor.visit_nanbox_f64_slot(callback);
}
}
});
}

/// Voice indices whose miniaudio end_callback fired on the audio thread.
/// We can't touch thread-locals from there; main-thread `drain_*` pulls
/// these into PENDING_ENDED on every hot-path entry point.
Expand Down Expand Up @@ -460,6 +488,7 @@ pub extern "C" fn perry_audio_unload(sound: f64) {

#[no_mangle]
pub extern "C" fn perry_audio_on_loaded(sound: f64, callback: f64) {
ensure_gc_scanner_registered();
let idx = match classify(sound) {
HandleKind::Sound(i) => i,
_ => return,
Expand Down Expand Up @@ -1174,6 +1203,7 @@ pub extern "C" fn perry_audio_get_position(playback: f64) -> f64 {

#[no_mangle]
pub extern "C" fn perry_audio_on_ended(playback: f64, callback: f64) {
ensure_gc_scanner_registered();
let idx = match classify(playback) {
HandleKind::Playback(i) => i,
_ => return,
Expand Down
42 changes: 42 additions & 0 deletions crates/perry-ffi/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,22 @@ impl<'a> GcRootVisitor<'a> {
)
}

/// Visit a raw const heap pointer slot.
///
/// Native UI backends commonly unbox a closure once and retain its code
/// pointer as `*const u8`. The collector treats const and mutable pointee
/// types identically; it only needs the address of the mutable pointer
/// *slot* so an evacuating collection can rewrite it.
///
/// Returns `true` when the runtime rewrote the slot to a forwarded address.
pub fn visit_raw_const_ptr_slot<T>(&mut self, slot: &mut *const T) -> bool {
(self.visit)(
FFI_ROOT_SLOT_RAW_MUT_PTR,
slot as *mut *const T as *mut c_void,
self.ctx,
)
}

/// Visit a NaN-boxed JS value stored as an `f64`.
///
/// Returns `true` when the runtime rewrote the slot to a forwarded address.
Expand Down Expand Up @@ -908,6 +924,32 @@ mod tests {
use super::*;
use std::time::Duration;

extern "C" fn rewrite_const_pointer_slot(
kind: u32,
slot: *mut c_void,
ctx: *mut c_void,
) -> bool {
assert_eq!(kind, FFI_ROOT_SLOT_RAW_MUT_PTR);
unsafe {
*(slot as *mut *const u8) = ctx as *const u8;
}
true
}

#[test]
fn const_pointer_root_slot_is_rewritten() {
let original = 1_u8;
let replacement = 2_u8;
let mut slot = &original as *const u8;
let mut visitor = GcRootVisitor::new(
rewrite_const_pointer_slot,
&replacement as *const u8 as *mut c_void,
);

assert!(visitor.visit_raw_const_ptr_slot(&mut slot));
assert_eq!(slot, &replacement as *const u8);
}

#[test]
fn round_trip_simple_value() {
let h = register_handle(42_i64);
Expand Down
14 changes: 14 additions & 0 deletions crates/perry-ui-android/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(crate) use perry_ffi::copy_string_from_raw as str_from_header;

/// Create an app. Stores config for deferred creation. Returns app handle (i64).
pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 {
crate::gc::ensure_registered();
let title = if title_ptr.is_null() {
"Perry App".to_string()
} else {
Expand Down Expand Up @@ -393,6 +394,19 @@ pub fn on_terminate(callback: f64) {
});
}

pub(crate) fn scan_android_app_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
ON_ACTIVATE_CALLBACK.with(|slot| {
if let Some(callback) = slot.borrow_mut().as_mut() {
visitor.visit_nanbox_f64_slot(callback);
}
});
ON_TERMINATE_CALLBACK.with(|slot| {
if let Some(callback) = slot.borrow_mut().as_mut() {
visitor.visit_nanbox_f64_slot(callback);
}
});
}

/// Called from JNI when Activity resumes.
pub fn handle_activate() {
ON_ACTIVATE_CALLBACK.with(|c| {
Expand Down
10 changes: 10 additions & 0 deletions crates/perry-ui-android/src/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ fn pump_microtasks() {
static CALLBACKS: Mutex<Option<HashMap<i64, f64>>> = Mutex::new(None);
static NEXT_KEY: AtomicI64 = AtomicI64::new(1);

pub(crate) fn scan_android_callback_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
if let Ok(mut callbacks) = CALLBACKS.lock() {
if let Some(callbacks) = callbacks.as_mut() {
for callback in callbacks.values_mut() {
visitor.visit_nanbox_f64_slot(callback);
}
}
}
}

/// Read the closure currently stored under `key`, or `None` if it's
/// been removed. Used by the pointer dispatcher to fetch the current
/// callback without going through `invoke*`.
Expand Down
23 changes: 23 additions & 0 deletions crates/perry-ui-android/src/gc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::sync::Once;

use perry_ffi::{gc_register_mutable_root_scanner_named, GcRootVisitor};

static GC_REGISTERED: Once = Once::new();

pub(crate) fn ensure_registered() {
perry_ui::ensure_gc_scanner_registered();
GC_REGISTERED.call_once(|| {
gc_register_mutable_root_scanner_named("perry-ui-android", scan_roots);
});
}

fn scan_roots(visitor: &mut GcRootVisitor<'_>) {
crate::app::scan_android_app_gc_roots(visitor);
crate::callback::scan_android_callback_gc_roots(visitor);
crate::media_playback::scan_android_media_playback_gc_roots(visitor);
crate::state::scan_android_state_gc_roots(visitor);
crate::widgets::lazyvstack::scan_android_lazyvstack_gc_roots(visitor);
crate::widgets::picker::scan_android_picker_gc_roots(visitor);
crate::widgets::webview::scan_android_webview_gc_roots(visitor);
crate::ws::scan_android_ws_gc_roots(visitor);
}
3 changes: 3 additions & 0 deletions crates/perry-ui-android/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod drag_drop;
pub mod fetch;
pub mod ffi;
pub mod file_dialog;
mod gc;
#[cfg(feature = "geisterhand")]
pub mod geisterhand_style;
pub mod geolocation;
Expand Down Expand Up @@ -132,6 +133,7 @@ pub(crate) fn catch_panic_void(name: &str, f: impl FnOnce() + std::panic::Unwind
/// Called by the JVM when the native library is loaded via System.loadLibrary().
#[no_mangle]
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, _reserved: *mut std::ffi::c_void) -> jni::sys::jint {
gc::ensure_registered();
unsafe {
__android_log_print(
3,
Expand Down Expand Up @@ -177,6 +179,7 @@ pub extern "C" fn Java_com_perry_app_PerryBridge_nativeInit(
mut env: jni::JNIEnv,
_class: jni::objects::JClass,
) {
gc::ensure_registered();
// Recover state that `JNI_OnLoad` would normally set up, in case a linked
// native library's own `JNI_OnLoad` shadowed Perry's (see
// `jni_bridge::ensure_vm` and `PERRY_DISABLE_MTE_CTOR`). The constructor
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-ui-android/src/media_playback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ thread_local! {
static PUMP_COUNTER: RefCell<u32> = const { RefCell::new(0) };
}

pub(crate) fn scan_android_media_playback_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
PLAYERS.with(|players| {
for player in players.borrow_mut().iter_mut().flatten() {
if let Some(callback) = player.on_state_change.as_mut() {
visitor.visit_nanbox_f64_slot(callback);
}
if let Some(callback) = player.on_time_update.as_mut() {
visitor.visit_nanbox_f64_slot(callback);
}
}
});
}

// ---------------------------------------------------------------------------
// String helpers
// ---------------------------------------------------------------------------
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-ui-android/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ thread_local! {
static TEXTFIELD_BINDINGS: RefCell<HashMap<i64, Vec<TextFieldBinding>>> = RefCell::new(HashMap::new());
}

pub(crate) fn scan_android_state_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
STATES.with(|states| {
for state in states.borrow_mut().iter_mut() {
visitor.visit_nanbox_f64_slot(&mut state.value);
}
});
FOR_EACH_BINDINGS.with(|bindings| {
for binding in bindings.borrow_mut().values_mut().flatten() {
visitor.visit_nanbox_f64_slot(&mut binding.render_closure);
}
});
ON_CHANGE_BINDINGS.with(|bindings| {
for binding in bindings.borrow_mut().values_mut().flatten() {
visitor.visit_nanbox_f64_slot(&mut binding.callback_ptr);
}
});
}

use perry_ffi::copy_string_from_raw as str_from_header;

/// Check if a f64 value is a NaN-boxed string. Accepts heap
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-ui-android/src/widgets/lazyvstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ thread_local! {
static LAZY_STATES: RefCell<HashMap<i64, LazyState>> = RefCell::new(HashMap::new());
}

pub(crate) fn scan_android_lazyvstack_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
LAZY_STATES.with(|states| {
for state in states.borrow_mut().values_mut() {
visitor.visit_nanbox_f64_slot(&mut state.render_closure);
}
});
}

pub fn create(count: f64, render_closure: f64) -> i64 {
// Create ScrollView
let scroll_handle = super::scrollview::create();
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-ui-android/src/widgets/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ thread_local! {
static PICKER_STATES: RefCell<HashMap<i64, PickerState>> = RefCell::new(HashMap::new());
}

pub(crate) fn scan_android_picker_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
PICKER_STATES.with(|states| {
for state in states.borrow_mut().values_mut() {
visitor.visit_nanbox_f64_slot(&mut state.on_change);
}
});
}

pub fn create(label_ptr: *const u8, on_change: f64, _style: i64) -> i64 {
let _label = unsafe { str_from_header(label_ptr) };
let mut env = jni_bridge::get_env();
Expand Down
15 changes: 15 additions & 0 deletions crates/perry-ui-android/src/widgets/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ thread_local! {
static WEBVIEW_STATES: RefCell<HashMap<i64, WebViewState>> = RefCell::new(HashMap::new());
}

pub(crate) fn scan_android_webview_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
WEBVIEW_STATES.with(|states| {
for state in states.borrow_mut().values_mut() {
visitor.visit_nanbox_f64_slot(&mut state.on_should_navigate);
visitor.visit_nanbox_f64_slot(&mut state.on_loaded);
visitor.visit_nanbox_f64_slot(&mut state.on_error);
}
});
EVAL_CALLBACKS.with(|callbacks| {
for callback in callbacks.borrow_mut().values_mut() {
visitor.visit_nanbox_f64_slot(callback);
}
});
}

use perry_ffi::copy_string_from_raw as str_from_header;

fn nanbox_str(s: &str) -> f64 {
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-ui-android/src/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ unsafe impl Send for SendPromise {}
/// The pump tick drains this and calls js_promise_resolve on the main thread.
static PENDING_RESOLVES: Mutex<Vec<(SendPromise, f64)>> = Mutex::new(Vec::new());

pub(crate) fn scan_android_ws_gc_roots(visitor: &mut perry_ffi::GcRootVisitor<'_>) {
if let Ok(mut pending) = PENDING_RESOLVES.lock() {
for (promise, value) in pending.iter_mut() {
visitor.visit_raw_mut_ptr_slot(&mut promise.0);
visitor.visit_nanbox_f64_slot(value);
}
}
}

/// Extract a Rust &str from a Perry StringHeader pointer.
use perry_ffi::copy_string_from_raw as str_from_header;

Expand Down
Loading
Loading