mirror of
https://github.com/lexmount/moli.git
synced 2026-10-13 00:01:13 +00:00
fix(renderer): retain detached document state until realm collection
Retain native DOM, Blob, AbortSignal and intrinsic interface backing while JavaScript still holds a retired realm. Retire active execution resources and trace wrapper and traversal-filter ownership through V8 so the last reference releases the native host without Rust-to-V8 root cycles. Cover retained native values, realm finalizers and traversal callback lifetimes. Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast.
This commit is contained in:
@@ -246,6 +246,32 @@ where
|
||||
Some((String::from_utf8_lossy(&bytes).into_owned(), mime_type))
|
||||
}
|
||||
|
||||
/// Revoke a departing context's URLs while preserving bytes retained by
|
||||
/// wrappers, readers, or URLs created by another context.
|
||||
pub fn retire_owner_resources(&self, owner_id: OwnerId) {
|
||||
let retired = {
|
||||
let mut urls = self.object_urls.lock();
|
||||
let mut retired = Vec::new();
|
||||
urls.retain(|_, state| {
|
||||
if state.owner_id == Some(owner_id) {
|
||||
retired.push(state.blob_id);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
retired
|
||||
};
|
||||
for blob_id in retired {
|
||||
self.release_blob_object_url_ref(blob_id);
|
||||
}
|
||||
for blob in self.blobs.lock().by_id.values_mut() {
|
||||
if blob.owner_id == Some(owner_id) {
|
||||
blob.owner_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove Blob/object URL entries owned by a context.
|
||||
pub fn cleanup_owner_resources(&self, owner_id: OwnerId) {
|
||||
let removed_blob_ids = {
|
||||
@@ -504,6 +530,30 @@ mod tests {
|
||||
assert!(store.blob_bytes_by_uuid_in_partition(&uuid, &10).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retiring_owner_preserves_wrappers_readers_and_foreign_urls() {
|
||||
let store = BlobStore::<u64, u64>::default();
|
||||
let blob = store.create_blob(Some(1), Some(10), b"retained".to_vec(), "text/plain".into());
|
||||
let own_url = store
|
||||
.create_object_url(Some(1), blob, "https://example.test")
|
||||
.unwrap();
|
||||
let foreign_url = store
|
||||
.create_object_url(Some(2), blob, "https://example.test")
|
||||
.unwrap();
|
||||
store.retain_blob_reader_ref(blob);
|
||||
store.retire_owner_resources(1);
|
||||
assert!(store.object_url_bytes_and_type(&own_url).is_none());
|
||||
assert_eq!(store.blob_bytes(blob).unwrap(), b"retained");
|
||||
store.release_blob_wrapper_ref(blob);
|
||||
store.release_blob_reader_ref(blob);
|
||||
assert_eq!(
|
||||
store.object_url_bytes_and_type(&foreign_url).unwrap().0,
|
||||
b"retained"
|
||||
);
|
||||
assert!(store.revoke_object_url(&foreign_url));
|
||||
assert!(store.blob_bytes(blob).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_owner_removes_owned_blobs_and_urls() {
|
||||
let store = BlobStore::<u64, u64>::default();
|
||||
|
||||
@@ -487,7 +487,7 @@ fn blob_mime_type(blob_id: BlobId) -> Option<String> {
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_owner_resources(owner_id: ResourceOwnerId) {
|
||||
blob_store().cleanup_owner_resources(owner_id);
|
||||
blob_store().retire_owner_resources(owner_id);
|
||||
}
|
||||
|
||||
fn release_blob_wrapper_ref(blob_id: BlobId) {
|
||||
|
||||
@@ -530,6 +530,16 @@ pub(crate) fn new_dom_exception_value<'s>(
|
||||
scope,
|
||||
"DOMException",
|
||||
)
|
||||
.or_else(|| {
|
||||
// A native exception must materialize its own realm's intrinsic.
|
||||
// Reading the public Window property can run author code or enter
|
||||
// an access check again after navigation detaches its global.
|
||||
crate::context_bootstrap::exposed_interfaces::ensure_intrinsic_interface_prototype(
|
||||
scope,
|
||||
"DOMException",
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
{
|
||||
let exception = v8::Object::new(scope);
|
||||
initialize_dom_exception(scope, exception, message, name);
|
||||
|
||||
@@ -30,3 +30,5 @@ pub(super) use metadata::constructor_spec_is_eager;
|
||||
#[cfg(test)]
|
||||
pub(crate) use metadata::dedicated_worker_lazy_interface_names_for_test;
|
||||
pub(in crate::context_bootstrap) use template_registry::ExposedInterfaceTemplateRegistry;
|
||||
|
||||
pub(crate) use realm_registry::retain_intrinsic_interfaces_in_realm;
|
||||
|
||||
@@ -20,9 +20,9 @@ pub(super) struct IntrinsicInterfaceRegistry {
|
||||
}
|
||||
|
||||
pub(super) struct RealmInterfaceObjects {
|
||||
constructor: v8::Global<v8::Object>,
|
||||
prototype: v8::Global<v8::Object>,
|
||||
public_interface: v8::Global<v8::Object>,
|
||||
constructor: crate::util::RealmObjectHandle,
|
||||
prototype: crate::util::RealmObjectHandle,
|
||||
public_interface: crate::util::RealmObjectHandle,
|
||||
}
|
||||
|
||||
impl IntrinsicInterfaceRegistry {
|
||||
@@ -156,9 +156,9 @@ impl IntrinsicInterfaceRegistry {
|
||||
RealmInterfaceEntry::Uninitialized | RealmInterfaceEntry::Materializing => {}
|
||||
}
|
||||
*slot = RealmInterfaceEntry::Ready(RealmInterfaceObjects {
|
||||
constructor: v8::Global::new(scope, constructor),
|
||||
prototype: v8::Global::new(scope, prototype),
|
||||
public_interface: v8::Global::new(scope, public_interface),
|
||||
constructor: crate::util::RealmObjectHandle::new(scope, constructor),
|
||||
prototype: crate::util::RealmObjectHandle::new(scope, prototype),
|
||||
public_interface: crate::util::RealmObjectHandle::new(scope, public_interface),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -172,7 +172,7 @@ impl IntrinsicInterfaceRegistry {
|
||||
let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else {
|
||||
return None;
|
||||
};
|
||||
Some(v8::Local::new(scope, &objects.constructor))
|
||||
objects.constructor.to_local(scope)
|
||||
}
|
||||
|
||||
pub(super) fn prototype<'s>(
|
||||
@@ -184,7 +184,7 @@ impl IntrinsicInterfaceRegistry {
|
||||
let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else {
|
||||
return None;
|
||||
};
|
||||
Some(v8::Local::new(scope, &objects.prototype))
|
||||
objects.prototype.to_local(scope)
|
||||
}
|
||||
|
||||
pub(super) fn public_interface<'s>(
|
||||
@@ -196,10 +196,25 @@ impl IntrinsicInterfaceRegistry {
|
||||
let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else {
|
||||
return None;
|
||||
};
|
||||
Some(v8::Local::new(scope, &objects.public_interface))
|
||||
objects.public_interface.to_local(scope)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn retain_intrinsic_interfaces_in_realm(scope: &mut v8::PinScope<'_, '_>) {
|
||||
let context = scope.get_current_context();
|
||||
let Some(registry) = context.get_slot::<IntrinsicInterfaceRegistry>() else {
|
||||
return;
|
||||
};
|
||||
for entry in registry.entries.borrow_mut().iter_mut() {
|
||||
if let RealmInterfaceEntry::Ready(objects) = entry {
|
||||
objects.constructor.retain_in_realm(scope);
|
||||
objects.prototype.retain_in_realm(scope);
|
||||
objects.public_interface.retain_in_realm(scope);
|
||||
}
|
||||
}
|
||||
crate::util::retain_context_v8_handle_state_for_safe_release(context, registry);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::pin::pin;
|
||||
|
||||
@@ -37,6 +37,19 @@ fn image_is_small_for_priority_boost(element: &crate::dom::native::Element) -> b
|
||||
// the host document state accessors, current-script/parser visibility
|
||||
// bookkeeping, and pending resource-load delivery that feed DCL/load decisions.
|
||||
impl DocumentRuntime {
|
||||
pub(crate) fn retire_v8_execution_state_for_context_teardown(&mut self) {
|
||||
self.timeouts = HostTimeoutScheduler::default();
|
||||
self.events = HostEventTargetRegistry::default();
|
||||
self.script_lifecycle.clear_for_document_replacement();
|
||||
self.root_document_parser = None;
|
||||
self.document_write_script_preload_scanner = None;
|
||||
self.document_write_script_preloads.clear();
|
||||
self.pending_parser_blocking_work = None;
|
||||
self.pending_parser_post_step_runtime_work = Default::default();
|
||||
self.reset_main_parser_continuation_for_document_replacement();
|
||||
self.main_document_script_preloads.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn bind_main_document_script_preload_store(
|
||||
&mut self,
|
||||
store: crate::runtime::DocumentScriptPreloadStore,
|
||||
|
||||
@@ -23,6 +23,9 @@ pub(crate) use statics::{
|
||||
};
|
||||
|
||||
const ABORT_SIGNAL_ID_SLOT: &str = "__lmAbortSignalId";
|
||||
const ABORT_SIGNAL_STATE_SLOT: &str = "__moliAbortSignalState";
|
||||
const ABORT_SIGNAL_ABORTED_SLOT: &str = "__moliAbortSignalAborted";
|
||||
const ABORT_SIGNAL_REASON_SLOT: &str = "__moliAbortSignalReason";
|
||||
const ABORT_CONTROLLER_ID_SLOT: &str = "__lmAbortControllerId";
|
||||
const ABORT_CONTROLLER_SIGNAL_SLOT: &str = "__lmAbortControllerSignal";
|
||||
|
||||
@@ -35,6 +38,9 @@ pub(crate) fn bind_signal_wrapper<'s>(
|
||||
return false;
|
||||
};
|
||||
set_private_value(scope, wrapper, ABORT_SIGNAL_ID_SLOT, id);
|
||||
if let Some(state) = get_private_value(scope, signal, ABORT_SIGNAL_STATE_SLOT) {
|
||||
set_private_value(scope, wrapper, ABORT_SIGNAL_STATE_SLOT, state);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -44,13 +50,14 @@ pub(super) struct AbortStore {
|
||||
next_controller_id: u32,
|
||||
signals: HashMap<u32, AbortSignalState>,
|
||||
controllers: HashMap<u32, u32>,
|
||||
detached: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AbortSignalState {
|
||||
signal: Option<v8::Global<v8::Object>>,
|
||||
signal: Option<crate::util::RealmObjectHandle>,
|
||||
aborted: bool,
|
||||
reason: Option<v8::Global<v8::Value>>,
|
||||
detached: bool,
|
||||
abort_algorithms: Vec<v8::Global<v8::Function>>,
|
||||
linked_target_listeners: Vec<AbortLinkedTargetListener>,
|
||||
// None for a source; Some (including empty) for a dependent signal's ordered roots.
|
||||
@@ -68,9 +75,82 @@ struct AbortLinkedTargetListener {
|
||||
struct AbortSignalDispatch {
|
||||
algorithms: Vec<v8::Global<v8::Function>>,
|
||||
linked_target_listeners: Vec<AbortLinkedTargetListener>,
|
||||
dispatch_event: bool,
|
||||
}
|
||||
|
||||
impl AbortStore {
|
||||
pub(super) fn clear_for_context_teardown(&mut self) {
|
||||
self.detached = true;
|
||||
for state in self.signals.values_mut() {
|
||||
state.detached = true;
|
||||
state.abort_algorithms.clear();
|
||||
state.linked_target_listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn retire_context<'s>(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
context: v8::Local<'s, v8::Context>,
|
||||
) {
|
||||
for state in self.signals.values_mut() {
|
||||
let Some(handle) = state.signal.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let Some(signal) = handle.to_local(scope) else {
|
||||
continue;
|
||||
};
|
||||
if signal.get_creation_context(scope) != Some(context) {
|
||||
continue;
|
||||
}
|
||||
handle.retain_in_realm(scope);
|
||||
state.detached = true;
|
||||
state.abort_algorithms.clear();
|
||||
state.linked_target_listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn signal_passive_state<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
signal: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
get_private_value(scope, signal, ABORT_SIGNAL_STATE_SLOT)
|
||||
.and_then(|state| v8::Local::<v8::Object>::try_from(state).ok())
|
||||
}
|
||||
|
||||
fn signal_aborted_from_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
signal: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
Self::signal_passive_state(scope, signal)
|
||||
.and_then(|state| get_private_value(scope, state, ABORT_SIGNAL_ABORTED_SLOT))
|
||||
.is_some_and(|value| value.is_true())
|
||||
}
|
||||
|
||||
fn signal_reason_from_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
signal: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
let state = Self::signal_passive_state(scope, signal)?;
|
||||
get_private_value(scope, state, ABORT_SIGNAL_REASON_SLOT)
|
||||
}
|
||||
|
||||
fn publish_signal_abort<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
signal: v8::Local<'s, v8::Object>,
|
||||
reason: v8::Local<'s, v8::Value>,
|
||||
) {
|
||||
if let Some(state) = Self::signal_passive_state(scope, signal) {
|
||||
set_private_value(
|
||||
scope,
|
||||
state,
|
||||
ABORT_SIGNAL_ABORTED_SLOT,
|
||||
v8::Boolean::new(scope, true).into(),
|
||||
);
|
||||
set_private_value(scope, state, ABORT_SIGNAL_REASON_SLOT, reason);
|
||||
}
|
||||
}
|
||||
|
||||
fn alloc_signal_id(&mut self) -> u32 {
|
||||
self.next_signal_id = self
|
||||
.next_signal_id
|
||||
@@ -102,9 +182,7 @@ impl AbortStore {
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
Self::signal_id_from_object(scope, object)
|
||||
.and_then(|id| self.signal_state(id))
|
||||
.is_some()
|
||||
Self::signal_id_from_object(scope, object).is_some()
|
||||
}
|
||||
|
||||
fn controller_id_from_object<'s>(
|
||||
@@ -127,13 +205,25 @@ impl AbortStore {
|
||||
let signal_id = self.alloc_signal_id();
|
||||
let mut state = AbortSignalState {
|
||||
aborted,
|
||||
detached: self.detached,
|
||||
..AbortSignalState::default()
|
||||
};
|
||||
state.signal = Some(v8::Global::new(scope, signal));
|
||||
if let Some(reason) = reason {
|
||||
state.reason = Some(v8::Global::new(scope, reason));
|
||||
}
|
||||
state.signal = Some(crate::util::RealmObjectHandle::new(scope, signal));
|
||||
self.signals.insert(signal_id, state);
|
||||
let passive_state = v8::Object::new(scope);
|
||||
set_private_value(
|
||||
scope,
|
||||
passive_state,
|
||||
ABORT_SIGNAL_ABORTED_SLOT,
|
||||
v8::Boolean::new(scope, aborted).into(),
|
||||
);
|
||||
set_private_value(
|
||||
scope,
|
||||
passive_state,
|
||||
ABORT_SIGNAL_REASON_SLOT,
|
||||
reason.unwrap_or_else(|| v8::undefined(scope).into()),
|
||||
);
|
||||
set_private_value(scope, signal, ABORT_SIGNAL_STATE_SLOT, passive_state.into());
|
||||
set_private_value(
|
||||
scope,
|
||||
signal,
|
||||
@@ -182,7 +272,7 @@ impl AbortStore {
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
self.signal_state(id)
|
||||
.and_then(|state| state.signal.as_ref())
|
||||
.map(|signal| v8::Local::new(scope, signal))
|
||||
.and_then(|signal| signal.to_local(scope))
|
||||
}
|
||||
|
||||
pub(super) fn signal_aborted<'s>(
|
||||
@@ -190,9 +280,7 @@ impl AbortStore {
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
signal: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
Self::signal_id_from_object(scope, signal)
|
||||
.and_then(|id| self.signal_state(id))
|
||||
.is_some_and(|state| state.aborted)
|
||||
Self::signal_aborted_from_object(scope, signal)
|
||||
}
|
||||
|
||||
pub(super) fn signal_reason<'s>(
|
||||
@@ -200,10 +288,7 @@ impl AbortStore {
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
signal: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
Self::signal_id_from_object(scope, signal)
|
||||
.and_then(|id| self.signal_state(id))
|
||||
.and_then(|state| state.reason.as_ref())
|
||||
.map(|reason| v8::Local::new(scope, reason))
|
||||
Self::signal_reason_from_object(scope, signal)
|
||||
}
|
||||
|
||||
pub(crate) fn register_abort_algorithm<'s>(
|
||||
@@ -218,6 +303,9 @@ impl AbortStore {
|
||||
let Some(state) = self.signal_state_mut(signal_id) else {
|
||||
return false;
|
||||
};
|
||||
if state.detached {
|
||||
return false;
|
||||
}
|
||||
state
|
||||
.abort_algorithms
|
||||
.push(v8::Global::new(scope, algorithm));
|
||||
@@ -283,12 +371,15 @@ impl AbortStore {
|
||||
reason: v8::Local<'s, v8::Value>,
|
||||
) -> Option<Vec<(u32, v8::Local<'s, v8::Object>)>> {
|
||||
let signal_id = Self::signal_id_from_object(scope, signal)?;
|
||||
if Self::signal_aborted_from_object(scope, signal) {
|
||||
return None;
|
||||
}
|
||||
Self::publish_signal_abort(scope, signal, reason);
|
||||
let state = self.signal_state_mut(signal_id)?;
|
||||
if state.aborted {
|
||||
return None;
|
||||
}
|
||||
state.aborted = true;
|
||||
state.reason = Some(v8::Global::new(scope, reason));
|
||||
let dependent_signals = state.dependent_signals.clone();
|
||||
// Publish every reason before any author callback can reenter abort.
|
||||
let mut signals = vec![(signal_id, signal)];
|
||||
@@ -300,9 +391,13 @@ impl AbortStore {
|
||||
continue;
|
||||
}
|
||||
state.aborted = true;
|
||||
state.reason = Some(v8::Global::new(scope, reason));
|
||||
if let Some(signal) = &state.signal {
|
||||
signals.push((dependent_signal_id, v8::Local::new(scope, signal)));
|
||||
if let Some(signal) = state
|
||||
.signal
|
||||
.as_ref()
|
||||
.and_then(|signal| signal.to_local(scope))
|
||||
{
|
||||
Self::publish_signal_abort(scope, signal, reason);
|
||||
signals.push((dependent_signal_id, signal));
|
||||
}
|
||||
}
|
||||
Some(signals)
|
||||
@@ -313,6 +408,7 @@ impl AbortStore {
|
||||
Some(AbortSignalDispatch {
|
||||
algorithms: std::mem::take(&mut state.abort_algorithms),
|
||||
linked_target_listeners: std::mem::take(&mut state.linked_target_listeners),
|
||||
dispatch_event: !state.detached,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -384,7 +480,9 @@ pub(crate) fn abort_signal<'s>(
|
||||
);
|
||||
}
|
||||
// Snapshot listeners after earlier signals and abort algorithms.
|
||||
abort_signal_events::dispatch_abort(scope, signal);
|
||||
if dispatch.dispatch_event {
|
||||
abort_signal_events::dispatch_abort(scope, signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,12 +47,12 @@ pub(crate) fn abort_controller_abort_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
let Some(_) = context_host_ptr_from_global_bridge(scope) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let controller = args.this();
|
||||
let Some(controller_id) = AbortStore::controller_id_from_object(scope, controller) else {
|
||||
let Some(_) = AbortStore::controller_id_from_object(scope, controller) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
@@ -67,26 +67,6 @@ pub(crate) fn abort_controller_abort_callback<'s>(
|
||||
} else {
|
||||
abort_error_value(scope)
|
||||
};
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
let Some(signal_id) = host
|
||||
.native_bridge_mut()
|
||||
.abort
|
||||
.controllers
|
||||
.get(&controller_id)
|
||||
.copied()
|
||||
else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
if host
|
||||
.native_bridge_mut()
|
||||
.abort
|
||||
.signal_state(signal_id)
|
||||
.is_some_and(|state| state.aborted)
|
||||
{
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
crate::native_bridge::abort::abort_signal(scope, signal, reason);
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
@@ -1,31 +1,11 @@
|
||||
use super::AbortStore;
|
||||
use crate::util::context_host_ptr_from_global_bridge;
|
||||
|
||||
pub(crate) fn abort_signal_aborted_getter_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
rv.set_bool(false);
|
||||
return;
|
||||
};
|
||||
let signal = args.this();
|
||||
if AbortStore::signal_id_from_object(scope, signal).is_none() {
|
||||
rv.set_bool(false);
|
||||
return;
|
||||
}
|
||||
// SAFETY: as_ptr() — this getter may be called during event dispatch
|
||||
// (re-entrant from another callback holding borrow_mut). See util.rs.
|
||||
let aborted = AbortStore::signal_id_from_object(scope, signal)
|
||||
.and_then(|id| {
|
||||
unsafe { &mut *host_ptr }
|
||||
.native_bridge_mut()
|
||||
.abort
|
||||
.signal_state(id)
|
||||
})
|
||||
.is_some_and(|state| state.aborted);
|
||||
rv.set_bool(aborted);
|
||||
rv.set_bool(AbortStore::signal_aborted_from_object(scope, args.this()));
|
||||
}
|
||||
|
||||
pub(crate) fn abort_signal_reason_getter_callback<'s>(
|
||||
@@ -33,29 +13,10 @@ pub(crate) fn abort_signal_reason_getter_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let signal = args.this();
|
||||
if AbortStore::signal_id_from_object(scope, signal).is_none() {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
match AbortStore::signal_reason_from_object(scope, args.this()) {
|
||||
Some(reason) => rv.set(reason),
|
||||
None => rv.set_undefined(),
|
||||
}
|
||||
let Some(reason) = AbortStore::signal_id_from_object(scope, signal)
|
||||
.and_then(|id| {
|
||||
unsafe { &mut *host_ptr }
|
||||
.native_bridge_mut()
|
||||
.abort
|
||||
.signal_state(id)
|
||||
})
|
||||
.and_then(|state| state.reason.as_ref())
|
||||
.map(|reason| v8::Local::new(scope, reason))
|
||||
else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
rv.set(reason);
|
||||
}
|
||||
|
||||
pub(crate) fn abort_signal_throw_if_aborted_callback<'s>(
|
||||
@@ -63,30 +24,12 @@ pub(crate) fn abort_signal_throw_if_aborted_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let signal = args.this();
|
||||
if AbortStore::signal_id_from_object(scope, signal).is_none() {
|
||||
if !AbortStore::signal_aborted_from_object(scope, signal) {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
let Some(reason) = AbortStore::signal_id_from_object(scope, signal)
|
||||
.and_then(|id| {
|
||||
unsafe { &mut *host_ptr }
|
||||
.native_bridge_mut()
|
||||
.abort
|
||||
.signal_state(id)
|
||||
})
|
||||
.filter(|state| state.aborted)
|
||||
.and_then(|state| state.reason.as_ref())
|
||||
.map(|reason| v8::Local::new(scope, reason))
|
||||
else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
// DOM requires throwing the stored abort reason itself. In particular, a
|
||||
// string reason remains a string rather than being wrapped in `Error`.
|
||||
let reason = AbortStore::signal_reason_from_object(scope, signal)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
scope.throw_exception(reason);
|
||||
}
|
||||
|
||||
@@ -91,20 +91,11 @@ pub(crate) fn abort_signal_any_callback<'s>(
|
||||
let signals = parsed.signals;
|
||||
|
||||
for source_signal in &signals {
|
||||
let Some(source_signal_id) = AbortStore::signal_id_from_object(scope, *source_signal)
|
||||
else {
|
||||
if !AbortStore::signal_aborted_from_object(scope, *source_signal) {
|
||||
continue;
|
||||
};
|
||||
let Some(reason) = host
|
||||
.native_bridge_mut()
|
||||
.abort
|
||||
.signal_state(source_signal_id)
|
||||
.filter(|state| state.aborted)
|
||||
.and_then(|state| state.reason.as_ref())
|
||||
.map(|reason| v8::Local::new(scope, reason))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
let reason = AbortStore::signal_reason_from_object(scope, *source_signal)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
crate::native_bridge::abort::abort_signal(scope, signal, reason);
|
||||
rv.set(signal.into());
|
||||
return;
|
||||
|
||||
@@ -81,7 +81,11 @@ impl JsContextHost {
|
||||
// The actual JsContextHost pointer for runtime_ptr_from_object compatibility.
|
||||
let bridge = unsafe { &mut (*host_ptr).bridge };
|
||||
bridge.install_global(scope, global, host_ptr, bridge_ref.as_external_ptr())?;
|
||||
crate::util::install_context_host_pointer_slot(scope.get_current_context(), host_ptr);
|
||||
crate::util::install_context_host_pointer_slot(
|
||||
scope.get_current_context(),
|
||||
host_ptr,
|
||||
unsafe { &*host_ptr }.context_host_lifecycle_handle(),
|
||||
);
|
||||
Ok(bridge_ref)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,26 @@ impl JsContextHost {
|
||||
closed_count
|
||||
}
|
||||
|
||||
pub(crate) fn close_broadcast_channels_for_context_token(
|
||||
&mut self,
|
||||
context_token: RuntimeObservableContextToken,
|
||||
) -> usize {
|
||||
let channel_ids = self
|
||||
.broadcast_channel_wrappers
|
||||
.iter()
|
||||
.filter_map(|(channel_id, entry)| {
|
||||
(entry.identity.realm_token() == context_token).then_some(*channel_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let closed_count = channel_ids.len();
|
||||
for channel_id in channel_ids {
|
||||
self.broadcast_channel_wrappers.remove(&channel_id);
|
||||
self.broadcast_channel_registry
|
||||
.close_broadcast_channel(channel_id);
|
||||
}
|
||||
closed_count
|
||||
}
|
||||
|
||||
pub(crate) fn register_broadcast_channel_wrapper(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
|
||||
@@ -962,7 +962,11 @@ impl JsContextHost {
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
crate::util::install_context_host_pointer_slot(context, self as *mut Self);
|
||||
crate::util::install_context_host_pointer_slot(
|
||||
context,
|
||||
self as *mut Self,
|
||||
self.context_host_lifecycle_handle(),
|
||||
);
|
||||
let previous = context.set_slot(Rc::new(ChildWindowProxyFacadeContextHandle(handle)));
|
||||
debug_assert!(previous.is_none());
|
||||
// Keep V8's unique default security token. A facade is not a real
|
||||
|
||||
@@ -212,6 +212,10 @@ impl JsContextHost {
|
||||
root_document_lifecycle: None,
|
||||
output_journal: None,
|
||||
page_context_resources_closed: false,
|
||||
context_host_lifecycle: Rc::new(Cell::new(crate::util::ContextHostLifecycle::Active)),
|
||||
deferred_context_host_release_queue: Default::default(),
|
||||
retained_document_runtime: None,
|
||||
published_document_host: false,
|
||||
page_default_context: None,
|
||||
v8_finalizers: crate::v8_finalizer::V8FinalizerRegistry::default(),
|
||||
bridge: NativeDomBridge::new(bindings),
|
||||
|
||||
@@ -820,6 +820,8 @@ pub(crate) struct JsContextHost {
|
||||
root_document_lifecycle: Option<RendererDocumentLifecycleJournalHandle>,
|
||||
output_journal: Option<crate::runtime::RendererTurnOutputJournal>,
|
||||
page_context_resources_closed: bool,
|
||||
context_host_lifecycle: Rc<Cell<crate::util::ContextHostLifecycle>>,
|
||||
deferred_context_host_release_queue: crate::script_vm::RendererDeferredContextHostReleaseQueue,
|
||||
page_default_context: Option<v8::Weak<v8::Context>>,
|
||||
pub(crate) v8_finalizers: crate::v8_finalizer::V8FinalizerRegistry,
|
||||
pub(super) bridge: NativeDomBridge,
|
||||
@@ -1099,6 +1101,8 @@ pub(crate) struct JsContextHost {
|
||||
stylo_computed_style_property_reads: Cell<u64>,
|
||||
#[cfg(test)]
|
||||
style_observation_environment_resolutions: Cell<u64>,
|
||||
retained_document_runtime: Option<Box<DocumentRuntime>>,
|
||||
published_document_host: bool,
|
||||
}
|
||||
|
||||
struct PendingTextControlChangeCommit {
|
||||
@@ -1253,16 +1257,66 @@ impl JsContextHost {
|
||||
self.app_manifest_link_change_epoch
|
||||
}
|
||||
|
||||
pub(crate) fn document_host_is_published(&self) -> bool {
|
||||
self.published_document_host
|
||||
}
|
||||
|
||||
pub(crate) fn publish_document_host(&mut self) {
|
||||
self.published_document_host = true;
|
||||
}
|
||||
|
||||
pub(crate) fn mark_page_context_detached(&self) {
|
||||
if self.context_host_lifecycle.get() == crate::util::ContextHostLifecycle::Active {
|
||||
self.context_host_lifecycle
|
||||
.set(crate::util::ContextHostLifecycle::Detached);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn context_host_lifecycle_handle(
|
||||
&self,
|
||||
) -> Rc<Cell<crate::util::ContextHostLifecycle>> {
|
||||
self.context_host_lifecycle.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn deferred_context_host_release_queue(
|
||||
&self,
|
||||
) -> crate::script_vm::RendererDeferredContextHostReleaseQueue {
|
||||
self.deferred_context_host_release_queue.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn adopt_retained_document_runtime(
|
||||
&mut self,
|
||||
document_runtime: Box<DocumentRuntime>,
|
||||
) {
|
||||
assert!(
|
||||
self.retained_document_runtime.is_none(),
|
||||
"JsContextHost must adopt its DocumentRuntime at most once"
|
||||
);
|
||||
assert!(
|
||||
std::ptr::eq(self.runtime.cast_const(), document_runtime.as_ref()),
|
||||
"retained DocumentRuntime transfer must preserve the host raw pointer"
|
||||
);
|
||||
self.retained_document_runtime = Some(document_runtime);
|
||||
}
|
||||
pub(crate) fn bind_deferred_context_host_release_queue(
|
||||
&mut self,
|
||||
queue: crate::script_vm::RendererDeferredContextHostReleaseQueue,
|
||||
) {
|
||||
self.deferred_context_host_release_queue = queue;
|
||||
}
|
||||
|
||||
pub(crate) fn close_page_context_resources_for_teardown(&mut self) {
|
||||
self.mark_page_context_detached();
|
||||
if self.page_context_resources_closed {
|
||||
return;
|
||||
}
|
||||
self.page_context_resources_closed = true;
|
||||
self.bridge.web_mcp.clear();
|
||||
self.retire_all_window_execution_context_resources_for_teardown();
|
||||
self.pending_history_traversal_admissions.clear();
|
||||
self.retire_all_document_resource_loaders();
|
||||
self.page_default_context = None;
|
||||
self.v8_finalizers.clear_for_context_teardown();
|
||||
|
||||
self.clear_pending_top_level_navigation();
|
||||
self.unregister_all_service_worker_child_clients();
|
||||
self.unregister_all_service_worker_popup_clients();
|
||||
@@ -1272,11 +1326,20 @@ impl JsContextHost {
|
||||
self.close_owned_broadcast_channels();
|
||||
self.close_owned_message_ports();
|
||||
self.shutdown_workers();
|
||||
self.output_journal = None;
|
||||
self.root_document_lifecycle = None;
|
||||
self.child_default_context_bootstrap = None;
|
||||
self.command_turn_output = None;
|
||||
self.lightweight_popup_browsing_contexts.clear();
|
||||
self.internal_inspector_value_references.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for JsContextHost {
|
||||
fn drop(&mut self) {
|
||||
self.close_page_context_resources_for_teardown();
|
||||
self.context_host_lifecycle
|
||||
.set(crate::util::ContextHostLifecycle::Destroyed);
|
||||
self.v8_finalizers.clear_for_context_teardown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,103 @@ impl PendingRuntimeObservableConsoleSourceEvent {
|
||||
}
|
||||
|
||||
impl JsContextHost {
|
||||
pub(crate) fn retire_all_window_execution_context_resources_for_teardown(&mut self) {
|
||||
let owners = self
|
||||
.window_execution_contexts
|
||||
.keys()
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
let mut realm_tokens = self
|
||||
.window_execution_contexts
|
||||
.values()
|
||||
.map(WindowExecutionContextBinding::realm_token)
|
||||
.chain(
|
||||
self.window_execution_context_realms
|
||||
.concrete_by_token
|
||||
.keys()
|
||||
.copied(),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
realm_tokens.sort_unstable();
|
||||
realm_tokens.dedup();
|
||||
|
||||
for realm_token in realm_tokens {
|
||||
self.cancel_timers_for_context_token(realm_token);
|
||||
self.retire_runtime_binding_context_token(realm_token);
|
||||
self.retire_image_decode_requests_for_context_token(realm_token);
|
||||
self.retire_webcrypto_context_token(realm_token);
|
||||
self.retire_opfs_context_token(realm_token);
|
||||
self.retire_workers_for_context_token(realm_token);
|
||||
self.disconnect_shared_worker_clients_for_context_token(realm_token);
|
||||
self.retire_window_xhrs_for_context_token(realm_token);
|
||||
self.retire_window_fetches_for_context_token(realm_token);
|
||||
self.retire_window_event_sources_for_context_token(realm_token);
|
||||
self.retire_message_ports_for_context_token(realm_token);
|
||||
self.retire_window_messages_for_context_token(realm_token);
|
||||
self.close_broadcast_channels_for_context_token(realm_token);
|
||||
self.retire_websockets_for_context_token(realm_token);
|
||||
self.retire_window_execution_contexts_for_context_token(realm_token);
|
||||
}
|
||||
|
||||
for owner in owners {
|
||||
self.cancel_window_execution_context_timers(owner);
|
||||
self.retire_webcrypto_execution_context_owner(owner);
|
||||
self.retire_opfs_execution_context_owner(owner);
|
||||
self.retire_workers_for_execution_context_owner(owner);
|
||||
self.disconnect_shared_worker_clients_for_execution_context_owner(owner);
|
||||
self.retire_window_xhrs_for_execution_context_owner(owner);
|
||||
self.retire_window_fetches_for_execution_context_owner(owner);
|
||||
self.retire_window_event_sources_for_execution_context_owner(owner);
|
||||
self.retire_window_messages_for_execution_context_owner(owner);
|
||||
self.close_broadcast_channels_for_execution_context_owner(owner);
|
||||
self.retire_websockets_for_execution_context_owner(owner);
|
||||
self.retire_image_decode_requests_for_execution_context_owner(owner);
|
||||
self.retire_message_ports_for_execution_context_owner(owner);
|
||||
self.retire_window_execution_context(owner);
|
||||
}
|
||||
|
||||
self.retire_v8_execution_state_for_context_teardown();
|
||||
|
||||
// A detached Document realm may keep this host and its native DOM
|
||||
// alive through the V8 Context slot. None of the host's active
|
||||
// execution registries may in turn keep that Context alive with a
|
||||
// strong Global handle: that would form an untraceable
|
||||
// Context -> Rust host -> Global -> Context cycle. Chromium retires
|
||||
// these LocalDOMWindow/ExecutionContext-owned services when the frame
|
||||
// is detached while ordinary retained Document/Node values remain
|
||||
// usable.
|
||||
drop(std::mem::take(&mut self.custom_elements));
|
||||
self.child_custom_elements.clear();
|
||||
self.scoped_custom_elements.clear();
|
||||
drop(std::mem::take(&mut self.custom_element_reactions));
|
||||
drop(std::mem::take(&mut self.observers));
|
||||
drop(std::mem::take(&mut self.child_window_proxy_records));
|
||||
self.pending_service_worker_registers.clear();
|
||||
self.pending_service_worker_unregisters.clear();
|
||||
self.pending_service_worker_ready.clear();
|
||||
self.service_worker_registration_watchers.clear();
|
||||
self.pending_window_messages.clear();
|
||||
drop(std::mem::take(&mut self.directory_reader_callbacks));
|
||||
drop(std::mem::take(&mut self.misc_platform_api_tasks));
|
||||
drop(std::mem::take(&mut self.file_entry_file_callbacks));
|
||||
drop(std::mem::take(&mut self.user_interaction_tasks));
|
||||
self.pending_image_load_events.clear();
|
||||
self.pending_media_load_sequences.clear();
|
||||
self.pending_text_track_load_sequences.clear();
|
||||
self.pending_media_text_track_gates.clear();
|
||||
self.resource_timing_buffers =
|
||||
super::resource_timing::SharedResourceTimingBufferRegistry::new();
|
||||
drop(std::mem::take(&mut self.history_queue));
|
||||
drop(std::mem::take(&mut self.rendering_updates));
|
||||
drop(std::mem::take(&mut self.view_transition_updates));
|
||||
drop(std::mem::take(&mut self.media_element_events));
|
||||
drop(std::mem::take(&mut self.element_toggle_events));
|
||||
drop(std::mem::take(&mut self.text_track_default_modes));
|
||||
self.child_window_event_listeners.clear();
|
||||
drop(std::mem::take(&mut self.event_callbacks));
|
||||
self.bridge.abort.clear_for_context_teardown();
|
||||
}
|
||||
|
||||
pub(crate) fn register_lightweight_popup_execution_context(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
|
||||
@@ -127,6 +127,24 @@ impl JsContextHost {
|
||||
retired
|
||||
}
|
||||
|
||||
pub(crate) fn retire_websockets_for_context_token(
|
||||
&mut self,
|
||||
context_token: super::super::RuntimeObservableContextToken,
|
||||
) -> usize {
|
||||
let socket_ids = self
|
||||
.websockets
|
||||
.iter()
|
||||
.filter_map(|(socket_id, state)| {
|
||||
(state.owner.realm_token() == context_token).then_some(*socket_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let retired = socket_ids.len();
|
||||
for socket_id in socket_ids {
|
||||
self.retire_websocket(socket_id);
|
||||
}
|
||||
retired
|
||||
}
|
||||
|
||||
pub(crate) fn retire_websocket(&mut self, socket_id: u64) -> bool {
|
||||
let Some(state) = self.websockets.remove(&socket_id) else {
|
||||
return false;
|
||||
|
||||
@@ -448,14 +448,14 @@ pub(crate) fn contexts_share_wrapper_world(
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BridgeCachedWrapper {
|
||||
wrapper: v8::Global<v8::Object>,
|
||||
wrapper: crate::util::RealmObjectHandle,
|
||||
creation_realm: Option<NonZeroU64>,
|
||||
}
|
||||
|
||||
impl BridgeCachedWrapper {
|
||||
fn new(scope: &mut v8::PinScope<'_, '_>, wrapper: v8::Local<'_, v8::Object>) -> Self {
|
||||
Self {
|
||||
wrapper: v8::Global::new(scope, wrapper),
|
||||
wrapper: crate::util::RealmObjectHandle::new(scope, wrapper),
|
||||
creation_realm: scope
|
||||
.get_current_context()
|
||||
.get_slot::<RuntimeObservableContextToken>()
|
||||
@@ -468,7 +468,7 @@ impl BridgeCachedWrapper {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BridgeContextWindowWrapper {
|
||||
wrapper: v8::Global<v8::Object>,
|
||||
wrapper: RefCell<crate::util::RealmObjectHandle>,
|
||||
}
|
||||
|
||||
fn context_window_wrapper<'s>(
|
||||
@@ -477,7 +477,7 @@ fn context_window_wrapper<'s>(
|
||||
scope
|
||||
.get_current_context()
|
||||
.get_slot::<BridgeContextWindowWrapper>()
|
||||
.map(|entry| v8::Local::new(scope, &entry.wrapper))
|
||||
.and_then(|entry| entry.wrapper.borrow().to_local(scope))
|
||||
}
|
||||
|
||||
fn set_context_window_wrapper(
|
||||
@@ -487,7 +487,7 @@ fn set_context_window_wrapper(
|
||||
let _ = scope
|
||||
.get_current_context()
|
||||
.set_slot(Rc::new(BridgeContextWindowWrapper {
|
||||
wrapper: v8::Global::new(scope, wrapper),
|
||||
wrapper: RefCell::new(crate::util::RealmObjectHandle::new(scope, wrapper)),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -514,7 +514,16 @@ impl BridgeContextWrapperCacheRetainForTest {
|
||||
self.cache.borrow().wrappers.len()
|
||||
}
|
||||
|
||||
pub(crate) fn wrapper_entry_count_for_realm(
|
||||
pub(crate) fn strong_wrapper_entry_count(&self) -> usize {
|
||||
self.cache
|
||||
.borrow()
|
||||
.wrappers
|
||||
.values()
|
||||
.filter(|entry| entry.wrapper.is_strong())
|
||||
.count()
|
||||
}
|
||||
|
||||
pub(crate) fn strong_wrapper_entry_count_for_realm(
|
||||
&self,
|
||||
realm_token: RuntimeObservableContextToken,
|
||||
) -> usize {
|
||||
@@ -522,7 +531,10 @@ impl BridgeContextWrapperCacheRetainForTest {
|
||||
.borrow()
|
||||
.wrappers
|
||||
.values()
|
||||
.filter(|entry| entry.creation_realm.map(NonZeroU64::get) == Some(realm_token.as_u64()))
|
||||
.filter(|entry| {
|
||||
entry.wrapper.is_strong()
|
||||
&& entry.creation_realm.map(NonZeroU64::get) == Some(realm_token.as_u64())
|
||||
})
|
||||
.count()
|
||||
}
|
||||
}
|
||||
@@ -541,18 +553,41 @@ pub(crate) fn clear_context_wrapper_cache_for_teardown(
|
||||
include_shared_default_world: bool,
|
||||
) {
|
||||
let context = scope.get_current_context();
|
||||
if !include_shared_default_world
|
||||
&& context
|
||||
crate::util::detach_document_page_context(context);
|
||||
if let Some(host) = crate::util::context_host_ptr_from_context_slot(context) {
|
||||
unsafe { &mut *host }
|
||||
.native_bridge_mut()
|
||||
.abort
|
||||
.retire_context(scope, context);
|
||||
}
|
||||
let all_entries = include_shared_default_world
|
||||
|| context
|
||||
.get_slot::<SharedDefaultWorldWrapperCache>()
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
.is_none();
|
||||
let realm = context
|
||||
.get_slot::<RuntimeObservableContextToken>()
|
||||
.as_deref()
|
||||
.map(|token| token.as_u64());
|
||||
if let Some(cache) = context.get_slot::<RefCell<BridgeContextWrapperCache>>() {
|
||||
let mut cache = cache.borrow_mut();
|
||||
cache.wrappers.clear();
|
||||
cache.live_collection_wrappers.clear();
|
||||
let mut entries = cache.borrow_mut();
|
||||
for entry in entries.wrappers.values_mut() {
|
||||
if all_entries || entry.creation_realm.map(NonZeroU64::get) == realm {
|
||||
entry.wrapper.retain_in_realm(scope);
|
||||
}
|
||||
}
|
||||
for entry in entries.live_collection_wrappers.values_mut() {
|
||||
if all_entries || entry.creation_realm.map(NonZeroU64::get) == realm {
|
||||
entry.wrapper.retain_in_realm(scope);
|
||||
}
|
||||
}
|
||||
drop(entries);
|
||||
crate::util::retain_context_v8_handle_state_for_safe_release(context, cache);
|
||||
}
|
||||
if let Some(wrapper) = context.get_slot::<BridgeContextWindowWrapper>() {
|
||||
wrapper.wrapper.borrow_mut().retain_in_realm(scope);
|
||||
crate::util::retain_context_v8_handle_state_for_safe_release(context, wrapper);
|
||||
}
|
||||
crate::context_bootstrap::exposed_interfaces::retain_intrinsic_interfaces_in_realm(scope);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -603,7 +638,7 @@ impl BridgeIdentityStore {
|
||||
.borrow()
|
||||
.wrappers
|
||||
.get(&reflector_id)
|
||||
.map(|entry| v8::Local::new(scope, &entry.wrapper))
|
||||
.and_then(|entry| entry.wrapper.to_local(scope))
|
||||
}
|
||||
|
||||
pub(super) fn cache_wrapper(
|
||||
@@ -642,7 +677,7 @@ impl BridgeIdentityStore {
|
||||
.borrow()
|
||||
.live_collection_wrappers
|
||||
.get(descriptor)
|
||||
.map(|entry| v8::Local::new(scope, &entry.wrapper))
|
||||
.and_then(|entry| entry.wrapper.to_local(scope))
|
||||
}
|
||||
|
||||
pub(super) fn cache_live_collection_wrapper(
|
||||
@@ -664,9 +699,11 @@ impl BridgeIdentityStore {
|
||||
let mut cache = self.default_world_wrapper_cache.borrow_mut();
|
||||
cache.wrappers.retain(|_, entry| {
|
||||
entry.creation_realm.map(NonZeroU64::get) != Some(realm_token.as_u64())
|
||||
|| !entry.wrapper.is_strong()
|
||||
});
|
||||
cache.live_collection_wrappers.retain(|_, entry| {
|
||||
entry.creation_realm.map(NonZeroU64::get) != Some(realm_token.as_u64())
|
||||
|| !entry.wrapper.is_strong()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -749,9 +786,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_wrapper_entry_uses_the_global_handle_niche() {
|
||||
fn cached_wrapper_entry_bounds_the_handle_and_realm_metadata() {
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
assert_eq!(size_of::<BridgeCachedWrapper>(), 24);
|
||||
assert_eq!(size_of::<BridgeCachedWrapper>(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -62,6 +62,7 @@ impl<V> DenseReflectorMap<V> {
|
||||
self.sparse.insert(id, value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn clear(&mut self) {
|
||||
self.dense_base = None;
|
||||
self.dense.clear();
|
||||
@@ -112,6 +113,13 @@ impl<V> DenseReflectorMap<V> {
|
||||
pub(super) fn values(&self) -> impl Iterator<Item = &V> {
|
||||
self.dense.iter().flatten().chain(self.sparse.values())
|
||||
}
|
||||
|
||||
pub(super) fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
|
||||
self.dense
|
||||
.iter_mut()
|
||||
.flatten()
|
||||
.chain(self.sparse.values_mut())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::v8_traced_webidl_callback::V8TracedWebIdlCallbackInterface;
|
||||
use crate::webidl::{PreparedWebIdlCallbackInterface, WebIdlCallbackInterface};
|
||||
use crate::{
|
||||
callback_invocation::{
|
||||
@@ -28,37 +29,44 @@ pub(super) enum TraversalFilterResult {
|
||||
|
||||
/// The callback-interface value owned by one TreeWalker or NodeIterator.
|
||||
///
|
||||
/// Traversal owns registration lifetime and reentrancy. The callback kernel
|
||||
/// owns the rooted callback plus its relevant/incumbent contexts. The renderer
|
||||
/// The wrapper owns a V8-traced callback carrier; the native registry only
|
||||
/// observes it weakly. The callback kernel roots invocation snapshots. The renderer
|
||||
/// identity is deliberately kept beside those engine-neutral facts so a
|
||||
/// detached or replaced Window cannot re-enter through a still-reachable
|
||||
/// traversal wrapper.
|
||||
pub(in crate::native_bridge) struct TraversalFilter {
|
||||
callback: WebIdlCallbackInterface,
|
||||
callback: v8::Weak<v8::Object>,
|
||||
execution_context: Option<WindowExecutionContextIdentity>,
|
||||
}
|
||||
|
||||
impl TraversalFilter {
|
||||
pub(super) fn new(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
pub(super) fn new<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
wrapper: v8::Local<'s, v8::Object>,
|
||||
callback: WebIdlCallbackInterface,
|
||||
) -> Self {
|
||||
let prepared = callback.prepare(scope);
|
||||
let relevant_context = prepared.relevant_context(scope);
|
||||
let execution_context = unsafe { &*runtime_ptr }
|
||||
.window_execution_context_identity_for_v8_context(scope, relevant_context);
|
||||
let carrier = V8TracedWebIdlCallbackInterface::new(scope, callback).into_object();
|
||||
crate::util::set_private_value(scope, wrapper, "__lmTraversalFilter", carrier.into());
|
||||
Self {
|
||||
callback,
|
||||
callback: v8::Weak::new(scope, carrier),
|
||||
execution_context,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn prepare(&self, scope: &mut v8::PinScope<'_, '_>) -> PreparedTraversalFilter {
|
||||
PreparedTraversalFilter {
|
||||
callback: self.callback.prepare(scope),
|
||||
pub(super) fn prepare(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> Option<PreparedTraversalFilter> {
|
||||
let carrier = self.callback.to_local(scope)?;
|
||||
Some(PreparedTraversalFilter {
|
||||
callback: V8TracedWebIdlCallbackInterface::from_object(carrier).prepare(scope),
|
||||
execution_context: self.execution_context,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ fn prepare_filter(
|
||||
filter: Option<&TraversalFilter>,
|
||||
) -> Option<Rc<PreparedTraversalFilter>> {
|
||||
let filter = filter?;
|
||||
Some(Rc::new(filter.prepare(scope)))
|
||||
Some(Rc::new(filter.prepare(scope)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -115,12 +115,12 @@ pub(in crate::native_bridge) fn build_node_iterator_wrapper<'s>(
|
||||
what_to_show: u32,
|
||||
filter: Option<WebIdlCallbackInterface>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let filter = filter.map(|filter| TraversalFilter::new(scope, runtime_ptr, filter));
|
||||
let id = bridge.register_node_iterator(root, what_to_show, filter);
|
||||
let template = bridge.node_iterator_wrapper_template();
|
||||
let wrapper = template
|
||||
.new_instance(scope)
|
||||
.expect("failed to instantiate NodeIterator wrapper");
|
||||
let filter = filter.map(|filter| TraversalFilter::new(scope, runtime_ptr, wrapper, filter));
|
||||
let id = bridge.register_node_iterator(root, what_to_show, filter);
|
||||
let runtime_external = v8::External::new(scope, runtime_ptr as *mut c_void);
|
||||
assert!(
|
||||
wrapper.set_internal_field(0, runtime_external.into()),
|
||||
@@ -142,12 +142,12 @@ pub(in crate::native_bridge) fn build_tree_walker_wrapper<'s>(
|
||||
what_to_show: u32,
|
||||
filter: Option<WebIdlCallbackInterface>,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let filter = filter.map(|filter| TraversalFilter::new(scope, runtime_ptr, filter));
|
||||
let id = bridge.register_tree_walker(root, what_to_show, filter);
|
||||
let template = bridge.tree_walker_wrapper_template();
|
||||
let wrapper = template
|
||||
.new_instance(scope)
|
||||
.expect("failed to instantiate TreeWalker wrapper");
|
||||
let filter = filter.map(|filter| TraversalFilter::new(scope, runtime_ptr, wrapper, filter));
|
||||
let id = bridge.register_tree_walker(root, what_to_show, filter);
|
||||
let runtime_external = v8::External::new(scope, runtime_ptr as *mut c_void);
|
||||
assert!(
|
||||
wrapper.set_internal_field(0, runtime_external.into()),
|
||||
|
||||
@@ -891,9 +891,10 @@ pub(crate) use standalone_test_harness::StandaloneScriptVmHarness;
|
||||
use crate::document_runtime::{DeferredPageTaskLane, FollowupPageTaskDisposition};
|
||||
use document_isolate::*;
|
||||
pub(crate) use document_isolate::{
|
||||
RendererDocumentIsolateBootstrap, RendererDocumentIsolateHandle,
|
||||
RendererDocumentIsolateReservationAccounting, RendererPageScriptEnvironment,
|
||||
ScriptVmDefaultWorldBootstrap, renderer_document_isolate_accounting_diagnostics,
|
||||
RendererDeferredContextHostReleaseQueue, RendererDocumentIsolateBootstrap,
|
||||
RendererDocumentIsolateHandle, RendererDocumentIsolateReservationAccounting,
|
||||
RendererPageScriptEnvironment, ScriptVmDefaultWorldBootstrap,
|
||||
renderer_document_isolate_accounting_diagnostics,
|
||||
};
|
||||
pub(crate) use eval_exec::execute_source_text_on_current_stack;
|
||||
pub(crate) use input_helpers::*;
|
||||
@@ -907,6 +908,7 @@ pub(crate) use runtime_bindings::PromiseRejectDispatchSlot;
|
||||
pub(crate) use runtime_bindings::perform_microtask_checkpoint_and_report_pending_promise_rejections;
|
||||
use runtime_bindings::*;
|
||||
pub(crate) use runtime_work::*;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
type ScriptGlobalsBaseline = Vec<String>;
|
||||
@@ -950,15 +952,46 @@ fn register_main_window_execution_context_for_bootstrap(
|
||||
.context("failed to register main LocalWindow execution context")
|
||||
}
|
||||
|
||||
pub(super) struct ScriptVmDocumentRuntimeOwner {
|
||||
document_runtime: Option<Box<DocumentRuntime>>,
|
||||
}
|
||||
|
||||
impl ScriptVmDocumentRuntimeOwner {
|
||||
fn new(document_runtime: Box<DocumentRuntime>) -> Self {
|
||||
Self {
|
||||
document_runtime: Some(document_runtime),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_for_retained_document_host(&mut self) -> Option<Box<DocumentRuntime>> {
|
||||
self.document_runtime.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ScriptVmDocumentRuntimeOwner {
|
||||
type Target = DocumentRuntime;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.document_runtime
|
||||
.as_deref()
|
||||
.expect("ScriptVm DocumentRuntime must remain owned until ScriptVm drop")
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ScriptVmDocumentRuntimeOwner {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.document_runtime
|
||||
.as_deref_mut()
|
||||
.expect("ScriptVm DocumentRuntime must remain owned until ScriptVm drop")
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ScriptVm {
|
||||
resource_owner_id: crate::resource_owner::ResourceOwnerId,
|
||||
/// Page/target-facing inspector state. This must drop before the renderer
|
||||
/// document isolate handle because the V8 inspector session touches the
|
||||
/// isolate-level backend while being destroyed.
|
||||
page_inspector: DocumentInspectorBinding,
|
||||
/// Handle to renderer-owner document isolate-level V8 state. Multiple page
|
||||
/// facades can share the holder while keeping page/context state here.
|
||||
renderer_document_isolate: RendererDocumentIsolateHandle,
|
||||
renderer_document_isolate_teardown: RendererDocumentIsolateTeardown,
|
||||
renderer_page_script_environment: Option<RendererPageScriptEnvironment>,
|
||||
page_default_context: v8::Global<v8::Context>,
|
||||
@@ -973,7 +1006,7 @@ pub(super) struct ScriptVm {
|
||||
// `JsContextHost` stores a non-owning pointer into `document_runtime`, so it
|
||||
// must be dropped before the runtime field during normal Rust field teardown.
|
||||
_context_host: Rc<RefCell<JsContextHost>>,
|
||||
pub(super) document_runtime: Box<DocumentRuntime>,
|
||||
pub(super) document_runtime: ScriptVmDocumentRuntimeOwner,
|
||||
post_domcontentloaded_page_task_tx: PageTaskSender,
|
||||
page_runtime_wake_tx: PageRuntimeWakeSender,
|
||||
queued_main_document_runtime_continuation_owner:
|
||||
@@ -1014,6 +1047,9 @@ pub(super) struct ScriptVm {
|
||||
#[cfg(test)]
|
||||
_page_task_residence_for_executor_test:
|
||||
Option<crate::page_task_queue::RendererPageTaskTestResidence>,
|
||||
// Native and V8 per-document state must drop before the final isolate
|
||||
// handle; retained realms then release their host through its GC queue.
|
||||
renderer_document_isolate: RendererDocumentIsolateHandle,
|
||||
}
|
||||
|
||||
impl moli_layout::GeometryProvider for ScriptVm {
|
||||
@@ -2010,6 +2046,11 @@ impl ScriptVmPageRealmBootstrap {
|
||||
top_level_storage_key,
|
||||
reserved_service_worker_client_id,
|
||||
)));
|
||||
context_host
|
||||
.borrow_mut()
|
||||
.bind_deferred_context_host_release_queue(
|
||||
renderer_document_isolate.deferred_context_host_release_queue(),
|
||||
);
|
||||
let main_document_owner = context_host
|
||||
.borrow()
|
||||
.current_main_document_task_owner()
|
||||
@@ -2383,7 +2424,7 @@ impl ScriptVmDefaultWorldBootstrap {
|
||||
page_default_runtime_observable_context_token: runtime_observable_context_token,
|
||||
root_frame_id,
|
||||
baseline_globals,
|
||||
document_runtime,
|
||||
document_runtime: ScriptVmDocumentRuntimeOwner::new(document_runtime),
|
||||
_context_host: context_host,
|
||||
page_context_cancel_tx,
|
||||
post_domcontentloaded_page_task_tx,
|
||||
@@ -2426,6 +2467,26 @@ impl ScriptVmDefaultWorldBootstrap {
|
||||
.borrow_mut()
|
||||
.bind_output_journal(environment.output_journal());
|
||||
}
|
||||
vm._context_host.borrow_mut().publish_document_host();
|
||||
vm.renderer_document_isolate
|
||||
.with_renderer_document_isolate_mut(|isolate| {
|
||||
let scope = pin!(v8::HandleScope::new(isolate));
|
||||
let scope = &mut scope.init();
|
||||
crate::util::retain_context_host_for_document_realm(
|
||||
v8::Local::new(scope, &vm.page_default_context),
|
||||
vm._context_host.clone(),
|
||||
vm.renderer_document_isolate
|
||||
.deferred_context_host_release_queue(),
|
||||
);
|
||||
for child in vm.prebootstrapped_child_default_contexts.borrow().values() {
|
||||
crate::util::retain_context_host_for_document_realm(
|
||||
v8::Local::new(scope, &child.context),
|
||||
vm._context_host.clone(),
|
||||
vm.renderer_document_isolate
|
||||
.deferred_context_host_release_queue(),
|
||||
);
|
||||
}
|
||||
});
|
||||
Ok(vm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::{
|
||||
cell::{OnceCell, RefCell},
|
||||
cell::{Cell, OnceCell, RefCell},
|
||||
rc::Rc,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
@@ -44,6 +44,67 @@ static DOCUMENT_ISOLATE_DESTROYED_COUNT: AtomicU64 = AtomicU64::new(0);
|
||||
static DOCUMENT_ISOLATE_LIVE_COUNT: AtomicU64 = AtomicU64::new(0);
|
||||
static DOCUMENT_ISOLATE_RESERVED_COUNT: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct RendererDeferredContextHostReleaseQueue {
|
||||
inner: Rc<RendererDeferredContextHostReleaseQueueInner>,
|
||||
}
|
||||
|
||||
struct RendererDeferredContextHostRelease {
|
||||
_host: Rc<RefCell<JsContextHost>>,
|
||||
retained_v8_handle_state: Vec<Box<dyn std::any::Any>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RendererDeferredContextHostRelease {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter
|
||||
.debug_struct("RendererDeferredContextHostRelease")
|
||||
.field(
|
||||
"retained_v8_handle_state_count",
|
||||
&self.retained_v8_handle_state.len(),
|
||||
)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RendererDeferredContextHostReleaseQueueInner {
|
||||
pending: RefCell<Vec<RendererDeferredContextHostRelease>>,
|
||||
isolate_shutting_down: Cell<bool>,
|
||||
}
|
||||
|
||||
impl RendererDeferredContextHostReleaseQueue {
|
||||
pub(crate) fn defer(
|
||||
&self,
|
||||
host: Rc<RefCell<JsContextHost>>,
|
||||
retained_v8_handle_state: Vec<Box<dyn std::any::Any>>,
|
||||
) {
|
||||
let release = RendererDeferredContextHostRelease {
|
||||
_host: host,
|
||||
retained_v8_handle_state,
|
||||
};
|
||||
if self.inner.isolate_shutting_down.get() {
|
||||
drop(release);
|
||||
return;
|
||||
}
|
||||
self.inner.pending.borrow_mut().push(release);
|
||||
}
|
||||
|
||||
fn drain_on_entered_isolate(&self) {
|
||||
loop {
|
||||
let pending = std::mem::take(&mut *self.inner.pending.borrow_mut());
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
drop(pending);
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_isolate_shutdown(&self) {
|
||||
self.drain_on_entered_isolate();
|
||||
self.inner.isolate_shutting_down.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn renderer_document_isolate_accounting_diagnostics()
|
||||
-> crate::runtime::RendererDocumentIsolateAccountingDiagnostics {
|
||||
crate::runtime::RendererDocumentIsolateAccountingDiagnostics {
|
||||
@@ -93,11 +154,8 @@ pub(super) struct ScriptVmPageRealmBootstrap {
|
||||
pub(super) resource_owner_id: ResourceOwnerId,
|
||||
pub(super) promise_reject_dispatch: PromiseRejectDispatchSlot,
|
||||
pub(super) page_inspector: DocumentInspectorBinding,
|
||||
pub(super) renderer_document_isolate: RendererDocumentIsolateHandle,
|
||||
pub(super) renderer_document_isolate_teardown: RendererDocumentIsolateTeardown,
|
||||
pub(super) document_runtime: Box<DocumentRuntime>,
|
||||
pub(super) root_frame_id: Option<String>,
|
||||
pub(super) context_host: Rc<RefCell<JsContextHost>>,
|
||||
pub(super) prebootstrapped_child_default_contexts: SharedPrebootstrappedChildDefaultContexts,
|
||||
pub(super) page_context_cancel_tx: RendererPageContextCancelSender,
|
||||
pub(super) post_domcontentloaded_page_task_tx: PageTaskSender,
|
||||
@@ -105,6 +163,12 @@ pub(super) struct ScriptVmPageRealmBootstrap {
|
||||
pub(super) storage_bucket_store: crate::context_bootstrap::SharedStorageBucketStore,
|
||||
pub(super) renderer_page_script_environment: Option<RendererPageScriptEnvironment>,
|
||||
pub(super) reuse_main_window_proxy: bool,
|
||||
// Partial bootstrap can drop before publication. Release bridge owners
|
||||
// before their native backing, and keep V8 alive until all of it
|
||||
// has been released.
|
||||
pub(super) context_host: Rc<RefCell<JsContextHost>>,
|
||||
pub(super) document_runtime: Box<DocumentRuntime>,
|
||||
pub(super) renderer_document_isolate: RendererDocumentIsolateHandle,
|
||||
}
|
||||
|
||||
pub(super) struct ScriptVmContextBootstrap {
|
||||
@@ -269,21 +333,24 @@ pub(crate) struct ScriptVmDefaultWorldBootstrap {
|
||||
pub(super) resource_owner_id: ResourceOwnerId,
|
||||
pub(super) promise_reject_dispatch: PromiseRejectDispatchSlot,
|
||||
pub(super) page_inspector: DocumentInspectorBinding,
|
||||
pub(super) renderer_document_isolate: RendererDocumentIsolateHandle,
|
||||
pub(super) renderer_document_isolate_teardown: RendererDocumentIsolateTeardown,
|
||||
pub(super) renderer_page_script_environment: Option<RendererPageScriptEnvironment>,
|
||||
pub(super) page_default_context: v8::Global<v8::Context>,
|
||||
pub(super) bridge_ref: JsContextHostBridgeRef,
|
||||
pub(super) runtime_observable_context_token: RuntimeObservableContextToken,
|
||||
pub(super) baseline_globals: super::ScriptGlobalsBaseline,
|
||||
pub(super) document_runtime: Box<DocumentRuntime>,
|
||||
pub(super) root_frame_id: Option<String>,
|
||||
pub(super) context_host: Rc<RefCell<JsContextHost>>,
|
||||
pub(super) prebootstrapped_child_default_contexts: SharedPrebootstrappedChildDefaultContexts,
|
||||
pub(super) page_context_cancel_tx: RendererPageContextCancelSender,
|
||||
pub(super) post_domcontentloaded_page_task_tx: PageTaskSender,
|
||||
pub(super) page_runtime_wake_tx: PageRuntimeWakeSender,
|
||||
pub(super) storage_bucket_store: crate::context_bootstrap::SharedStorageBucketStore,
|
||||
// Partial bootstrap can drop before publication. Release bridge owners
|
||||
// before their native backing, and keep V8 alive until all of it
|
||||
// has been released.
|
||||
pub(super) context_host: Rc<RefCell<JsContextHost>>,
|
||||
pub(super) document_runtime: Box<DocumentRuntime>,
|
||||
pub(super) renderer_document_isolate: RendererDocumentIsolateHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -342,6 +409,7 @@ impl RendererDocumentIsolateTeardown {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RendererDocumentIsolateHandle {
|
||||
inner: Rc<RefCell<RendererDocumentIsolateHolder>>,
|
||||
deferred_context_host_releases: RendererDeferredContextHostReleaseQueue,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RendererDocumentIsolateHandle {
|
||||
@@ -352,6 +420,12 @@ impl std::fmt::Debug for RendererDocumentIsolateHandle {
|
||||
}
|
||||
|
||||
impl RendererDocumentIsolateHandle {
|
||||
pub(crate) fn deferred_context_host_release_queue(
|
||||
&self,
|
||||
) -> RendererDeferredContextHostReleaseQueue {
|
||||
self.deferred_context_host_releases.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_standalone_without_owner_reservation_for_test(
|
||||
v8_foreground_task_sender: RendererPageV8ForegroundTaskSender,
|
||||
@@ -384,7 +458,11 @@ impl RendererDocumentIsolateHandle {
|
||||
foreground_wake,
|
||||
devtools_target_shutdown_registry,
|
||||
)?;
|
||||
let deferred_context_host_releases = renderer_document_isolate
|
||||
.deferred_context_host_releases
|
||||
.clone();
|
||||
let renderer_document_isolate = Self {
|
||||
deferred_context_host_releases,
|
||||
inner: Rc::new(RefCell::new(renderer_document_isolate)),
|
||||
};
|
||||
let isolate_backend = renderer_document_isolate.inspector_isolate_backend_handle();
|
||||
@@ -445,7 +523,12 @@ impl RendererDocumentIsolateHandle {
|
||||
let inspector_backend = inspector_backend
|
||||
.as_mut()
|
||||
.expect("document isolate Inspector backend missing before ScriptVm drop");
|
||||
with_entered_owned_isolate(isolate, |isolate| op(isolate, inspector_backend))
|
||||
with_entered_owned_isolate(isolate, |isolate| {
|
||||
let result = op(isolate, inspector_backend);
|
||||
self.deferred_context_host_releases
|
||||
.drain_on_entered_isolate();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn with_entered_renderer_document_isolate_and_inspector_mut<T>(
|
||||
@@ -461,7 +544,12 @@ impl RendererDocumentIsolateHandle {
|
||||
let inspector_backend = inspector_backend
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("document isolate Inspector backend unavailable"))?;
|
||||
with_entered_owned_isolate(isolate, |isolate| op(isolate, inspector_backend))
|
||||
with_entered_owned_isolate(isolate, |isolate| {
|
||||
let result = op(isolate, inspector_backend);
|
||||
self.deferred_context_host_releases
|
||||
.drain_on_entered_isolate();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn with_renderer_document_isolate_mut<T>(
|
||||
@@ -469,7 +557,12 @@ impl RendererDocumentIsolateHandle {
|
||||
op: impl FnOnce(&mut v8::OwnedIsolate) -> T,
|
||||
) -> T {
|
||||
let mut holder = self.inner.borrow_mut();
|
||||
with_entered_owned_isolate(&mut holder.isolate, op)
|
||||
with_entered_owned_isolate(&mut holder.isolate, |isolate| {
|
||||
let result = op(isolate);
|
||||
self.deferred_context_host_releases
|
||||
.drain_on_entered_isolate();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn with_entered_renderer_document_isolate<T>(
|
||||
@@ -477,7 +570,12 @@ impl RendererDocumentIsolateHandle {
|
||||
op: impl FnOnce(&mut v8::OwnedIsolate) -> Result<T>,
|
||||
) -> Result<T> {
|
||||
let mut holder = self.inner.borrow_mut();
|
||||
with_entered_owned_isolate(&mut holder.isolate, op)
|
||||
with_entered_owned_isolate(&mut holder.isolate, |isolate| {
|
||||
let result = op(isolate);
|
||||
self.deferred_context_host_releases
|
||||
.drain_on_entered_isolate();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn with_entered_renderer_document_isolate_and_bootstrap<T>(
|
||||
@@ -488,7 +586,12 @@ impl RendererDocumentIsolateHandle {
|
||||
let RendererDocumentIsolateHolder {
|
||||
isolate, bootstrap, ..
|
||||
} = &mut *holder;
|
||||
with_entered_owned_isolate(isolate, |isolate| op(isolate, &*bootstrap))
|
||||
with_entered_owned_isolate(isolate, |isolate| {
|
||||
let result = op(isolate, &*bootstrap);
|
||||
self.deferred_context_host_releases
|
||||
.drain_on_entered_isolate();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn unregister_renderer_document_isolate_platform(&self) {
|
||||
@@ -506,6 +609,7 @@ impl RendererDocumentIsolateHandle {
|
||||
}
|
||||
|
||||
pub(super) struct RendererDocumentIsolateHolder {
|
||||
deferred_context_host_releases: RendererDeferredContextHostReleaseQueue,
|
||||
// Unregister before destroying the Inspector backend so owner shutdown
|
||||
// never observes a target whose isolate has already been disposed.
|
||||
_devtools_target_shutdown_registration: Option<RendererDevToolsTargetShutdownRegistration>,
|
||||
@@ -675,6 +779,7 @@ impl RendererDocumentIsolateHolder {
|
||||
isolate: v8::OwnedIsolate,
|
||||
) -> Self {
|
||||
Self {
|
||||
deferred_context_host_releases: RendererDeferredContextHostReleaseQueue::default(),
|
||||
_devtools_target_shutdown_registration: devtools_target_shutdown_registration,
|
||||
inspector_backend: Some(inspector_backend),
|
||||
bootstrap,
|
||||
@@ -694,6 +799,7 @@ impl Drop for RendererDocumentIsolateHolder {
|
||||
unsafe {
|
||||
self.isolate.enter();
|
||||
}
|
||||
self.deferred_context_host_releases.begin_isolate_shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,5 +19,10 @@ impl Drop for ScriptVm {
|
||||
.deactivate_page_vm_binding_for_teardown();
|
||||
|
||||
self.page_default_bridge_ref.take();
|
||||
if let Some(runtime) = self.document_runtime.take_for_retained_document_host() {
|
||||
self._context_host
|
||||
.borrow_mut()
|
||||
.adopt_retained_document_runtime(runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +406,8 @@ impl ScriptVm {
|
||||
Some(context)
|
||||
}
|
||||
Some(context) => {
|
||||
let context_ptr = &context.context as *const v8::Global<v8::Context>;
|
||||
self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false);
|
||||
self.cancel_history_traversals_for_retiring_window(
|
||||
crate::native_bridge::WindowExecutionContextOwner::Frame(
|
||||
context.local_window_id,
|
||||
@@ -555,6 +557,8 @@ impl ScriptVm {
|
||||
};
|
||||
if !stale_prebootstrapped_contexts.is_empty() {
|
||||
for (_, context) in &stale_prebootstrapped_contexts {
|
||||
let context_ptr = &context.context as *const v8::Global<v8::Context>;
|
||||
self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false);
|
||||
self.cancel_history_traversals_for_retiring_window(
|
||||
crate::native_bridge::WindowExecutionContextOwner::Frame(
|
||||
context.local_window_id,
|
||||
@@ -623,6 +627,8 @@ impl ScriptVm {
|
||||
let Some(context) = self.child_frame_realm_store.remove(&execution_context_id) else {
|
||||
return;
|
||||
};
|
||||
let context_ptr: *const v8::Global<v8::Context> = &context.context as *const _;
|
||||
self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false);
|
||||
self.cancel_history_traversals_for_retiring_window(
|
||||
crate::native_bridge::WindowExecutionContextOwner::Frame(context.local_window_id),
|
||||
);
|
||||
@@ -696,8 +702,6 @@ impl ScriptVm {
|
||||
retired_timer_count,
|
||||
"retired child Runtime binding context"
|
||||
);
|
||||
let context_ptr: *const v8::Global<v8::Context> = &context.context as *const _;
|
||||
self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false);
|
||||
assert!(
|
||||
self.page_inspector
|
||||
.destroy_context_registration(context.inspector_context_registration_id),
|
||||
@@ -806,6 +810,8 @@ impl ScriptVm {
|
||||
.destroy_context_registration(context.inspector_context_registration_id),
|
||||
"isolated context must retain its document-owned Inspector registration"
|
||||
);
|
||||
let context_ptr: *const v8::Global<v8::Context> = &context.context as *const _;
|
||||
self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false);
|
||||
// V8 Inspector consumes the still-identifiable realm while processing
|
||||
// `context_destroyed` (including Runtime lifecycle projection for
|
||||
// named child worlds). Only after that notification may the strict
|
||||
|
||||
@@ -363,6 +363,13 @@ impl ScriptVmContextBootstrap {
|
||||
if let Some(registration) = realm_registration.as_mut() {
|
||||
registration.commit();
|
||||
}
|
||||
// Child Window exposure can reach bootstrap while the caller holds
|
||||
// the host's RefCell borrow. Follow the bootstrap's raw host access
|
||||
// discipline and end each native read before touching V8 again.
|
||||
if unsafe { &*host_ptr }.document_host_is_published() {
|
||||
let queue = unsafe { &*host_ptr }.deferred_context_host_release_queue();
|
||||
crate::util::retain_context_host_for_document_realm(local_context, context_host, queue);
|
||||
}
|
||||
Ok(Self {
|
||||
context: v8::Global::new(scope, local_context),
|
||||
runtime_observable_context_token,
|
||||
|
||||
@@ -2986,7 +2986,8 @@ async fn child_default_bridge_ref_is_released_on_child_context_teardown() {
|
||||
)
|
||||
.expect("child wrapper cache setup should evaluate");
|
||||
assert_eq!(child_wrappers, "child-wrappers");
|
||||
let child_wrapper_count = retained_child_cache.wrapper_entry_count_for_realm(child_realm_token);
|
||||
let child_wrapper_count =
|
||||
retained_child_cache.strong_wrapper_entry_count_for_realm(child_realm_token);
|
||||
assert!(
|
||||
child_wrapper_count >= 1,
|
||||
"child context wrappers should populate its default-world cache partition: {child_wrapper_count}"
|
||||
@@ -2999,7 +3000,8 @@ async fn child_default_bridge_ref_is_released_on_child_context_teardown() {
|
||||
)
|
||||
})
|
||||
.expect("top default context token should be readable");
|
||||
let top_wrapper_count = retained_child_cache.wrapper_entry_count_for_realm(top_realm_token);
|
||||
let top_wrapper_count =
|
||||
retained_child_cache.strong_wrapper_entry_count_for_realm(top_realm_token);
|
||||
assert!(
|
||||
top_wrapper_count >= 1,
|
||||
"the shared default-world cache should contain live top-context wrappers"
|
||||
@@ -3031,12 +3033,13 @@ async fn child_default_bridge_ref_is_released_on_child_context_teardown() {
|
||||
"removed iframe should not keep a live child default context"
|
||||
);
|
||||
assert_eq!(
|
||||
retained_child_cache.wrapper_entry_count_for_realm(child_realm_token),
|
||||
retained_child_cache.strong_wrapper_entry_count_for_realm(child_realm_token),
|
||||
0,
|
||||
"destroying a child default context must retire its strong wrapper entries"
|
||||
);
|
||||
assert!(
|
||||
retained_child_cache.wrapper_entry_count_for_realm(top_realm_token) >= top_wrapper_count,
|
||||
retained_child_cache.strong_wrapper_entry_count_for_realm(top_realm_token)
|
||||
>= top_wrapper_count,
|
||||
"destroying a child context must preserve wrappers owned by the live top realm"
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,5 +1,77 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn retained_document_realm_keeps_native_values_until_the_last_v8_reference() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://retained-document.test/old",
|
||||
"<!doctype html><p>old document</p>",
|
||||
);
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.savedNode = document.querySelector('p');
|
||||
globalThis.savedDecoder = new TextDecoder();
|
||||
globalThis.savedBlob = new Blob(['retained blob']);
|
||||
globalThis.savedController = new AbortController();
|
||||
globalThis.savedSignal = savedController.signal;
|
||||
globalThis.savedComposite = AbortSignal.any([savedSignal]);
|
||||
globalThis.abortEventRan = false;
|
||||
savedSignal.onabort = () => { abortEventRan = true; };
|
||||
globalThis.savedReason = {kind: 'old realm'};
|
||||
globalThis.savedFunction = () => [savedNode.textContent, document.URL];
|
||||
'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let weak_host = vm.context_host_weak_for_test();
|
||||
let isolate = vm.renderer_document_isolate.clone();
|
||||
let context = vm.page_default_context.clone();
|
||||
drop(vm);
|
||||
assert!(
|
||||
weak_host.upgrade().is_some(),
|
||||
"retained realm owns its native DOM"
|
||||
);
|
||||
|
||||
let value = isolate.with_renderer_document_isolate_mut(|isolate| {
|
||||
let scope = std::pin::pin!(v8::HandleScope::new(isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = v8::Local::new(scope, &context);
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let source = crate::util::v8str(scope, r#"
|
||||
savedNode.textContent = 'retained';
|
||||
savedController.abort(savedReason);
|
||||
JSON.stringify([
|
||||
savedFunction(),
|
||||
savedDecoder.decode(new Uint8Array([65])),
|
||||
savedBlob.size,
|
||||
savedSignal.aborted,
|
||||
savedSignal.reason === savedReason,
|
||||
savedComposite.aborted && savedComposite.reason === savedReason,
|
||||
abortEventRan,
|
||||
savedNode === document.querySelector('p'),
|
||||
(() => { try { savedSignal.throwIfAborted(); } catch (e) { return e === savedReason; } })()
|
||||
])
|
||||
"#);
|
||||
let script = v8::Script::compile(scope, source, None).expect("retained realm compiles");
|
||||
crate::script_execution::execute_compiled_script(scope, script)
|
||||
.expect("retained native values remain usable")
|
||||
.to_rust_string_lossy(scope)
|
||||
});
|
||||
assert_eq!(
|
||||
value,
|
||||
r#"[["retained","https://retained-document.test/old"],"A",13,true,true,true,false,true,true]"#
|
||||
);
|
||||
|
||||
isolate.with_renderer_document_isolate_mut(|isolate| {
|
||||
drop(context);
|
||||
isolate.low_memory_notification();
|
||||
});
|
||||
isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification());
|
||||
assert!(
|
||||
weak_host.upgrade().is_none(),
|
||||
"last V8 reference releases native DOM outside GC"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_binding_calls_freeze_the_invoking_realm_generation() {
|
||||
let mut vm = new_storage_test_vm("https://runtime-binding-source-realm.test/");
|
||||
@@ -307,23 +379,33 @@ fn page_context_teardown_releases_opfs_handle_and_directory_iterator_registratio
|
||||
|
||||
vm.close_page_context_resources_for_context_teardown();
|
||||
|
||||
let handles = vm._context_host.borrow().opfs_handle_registry().unwrap();
|
||||
let iterators = vm
|
||||
._context_host
|
||||
.borrow()
|
||||
.opfs_directory_iterator_registry()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.opfs_handle_registry()
|
||||
.expect("OPFS handle registry remains owned until host teardown")
|
||||
.len(),
|
||||
0,
|
||||
"page teardown must run handle finalizers before isolate teardown"
|
||||
handles.len(),
|
||||
1,
|
||||
"retained handle remains backed while its realm is alive"
|
||||
);
|
||||
assert_eq!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.opfs_directory_iterator_registry()
|
||||
.expect("OPFS iterator registry remains owned until host teardown")
|
||||
.len(),
|
||||
iterators.len(),
|
||||
16,
|
||||
"retained iterators remain backed while their realm is alive"
|
||||
);
|
||||
assert_eq!(vm.eval("__opfsRoot.kind").unwrap(), "directory");
|
||||
drop(vm);
|
||||
assert_eq!(
|
||||
handles.len(),
|
||||
0,
|
||||
"page teardown must run iterator finalizers before isolate teardown"
|
||||
"final Context release retires native handle registrations"
|
||||
);
|
||||
assert_eq!(
|
||||
iterators.len(),
|
||||
0,
|
||||
"final Context release retires native iterator registrations"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
@@ -802,7 +884,7 @@ fn promise_reject_context_slot_does_not_retain_context_host_after_script_vm_drop
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn context_wrapper_cache_is_cleared_on_script_vm_teardown() {
|
||||
fn context_wrapper_cache_releases_native_roots_on_script_vm_teardown() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://wrapper-cache-retention.test/",
|
||||
"<!doctype html><main></main>",
|
||||
@@ -846,7 +928,7 @@ fn context_wrapper_cache_is_cleared_on_script_vm_teardown() {
|
||||
|
||||
drop(vm);
|
||||
assert_eq!(
|
||||
retained_cache.wrapper_entry_count(),
|
||||
retained_cache.strong_wrapper_entry_count(),
|
||||
0,
|
||||
"page context teardown must clear strong wrapper cache entries before contexts are dropped"
|
||||
);
|
||||
@@ -890,7 +972,7 @@ fn script_vm_page_context_teardown_is_idempotent() {
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_context_teardown_releases_all_context_owned_v8_finalizers() {
|
||||
fn page_context_teardown_preserves_finalizers_for_retained_native_objects() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://v8-finalizer-teardown.test/",
|
||||
"<!doctype html><body></body>",
|
||||
@@ -933,10 +1015,9 @@ fn page_context_teardown_releases_all_context_owned_v8_finalizers() {
|
||||
);
|
||||
|
||||
vm.close_page_context_resources_for_context_teardown();
|
||||
assert_eq!(
|
||||
vm._context_host.borrow().v8_finalizers.len(),
|
||||
0,
|
||||
"page context teardown must reset every weak handle before isolate teardown"
|
||||
assert!(
|
||||
vm._context_host.borrow().v8_finalizers.len() >= 128,
|
||||
"retiring active execution must preserve native objects retained by author code"
|
||||
);
|
||||
assert_eq!(
|
||||
vm._context_host
|
||||
@@ -947,7 +1028,13 @@ fn page_context_teardown_releases_all_context_owned_v8_finalizers() {
|
||||
);
|
||||
|
||||
vm.close_page_context_resources_for_context_teardown();
|
||||
assert_eq!(vm.eval("__finalizerObjects[2].size").unwrap(), "9");
|
||||
let weak_host = vm.context_host_weak_for_test();
|
||||
drop(vm);
|
||||
assert!(
|
||||
weak_host.upgrade().is_none(),
|
||||
"final isolate release must drop the native host"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn embedded_frame_owners_create_child_contexts_only_for_document_content() {
|
||||
@@ -1737,3 +1824,97 @@ fn frame_owner_content_accessors_live_on_exact_owner_prototypes() {
|
||||
r#"{"descriptors":[{"contentDocument":{"get":"function","set":"undefined","enumerable":true,"configurable":true},"contentWindow":{"get":"function","set":"undefined","enumerable":true,"configurable":true}},{"contentDocument":{"get":"function","set":"undefined","enumerable":true,"configurable":true},"contentWindow":{"get":"function","set":"undefined","enumerable":true,"configurable":true}},{"contentDocument":{"get":"function","set":"undefined","enumerable":true,"configurable":true},"contentWindow":{"get":"function","set":"undefined","enumerable":true,"configurable":true}}],"sameOriginValues":[true,true,true],"brandErrors":[["TypeError","TypeError"],["TypeError","TypeError"],["TypeError","TypeError"]],"absentFromBase":true}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unreferenced_traversal_filters_release_retired_document() {
|
||||
for method in ["createTreeWalker", "createNodeIterator"] {
|
||||
let mut vm = new_parsed_test_vm("https://traversal.test/old", "<body>old document</body>");
|
||||
vm.eval(&format!(
|
||||
"document.{method}(document, NodeFilter.SHOW_ALL, () => NodeFilter.FILTER_ACCEPT); 1"
|
||||
))
|
||||
.expect("unreferenced traversal setup");
|
||||
let host = vm.context_host_weak_for_test();
|
||||
let isolate = vm.renderer_document_isolate.clone();
|
||||
drop(vm);
|
||||
for _ in 0..3 {
|
||||
isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification());
|
||||
}
|
||||
assert!(
|
||||
host.upgrade().is_none(),
|
||||
"{method}: an unreferenced filter must not retain its retired Document host"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_traversal_filters_release_host_after_last_reference() {
|
||||
for method in ["createTreeWalker", "createNodeIterator"] {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://retained-traversal.test/old",
|
||||
"<body>old document</body>",
|
||||
);
|
||||
vm.eval(&format!(
|
||||
r#"
|
||||
window.retainedFilter = () => NodeFilter.FILTER_ACCEPT;
|
||||
window.retainedTraversal = document.{method}(
|
||||
document.body, NodeFilter.SHOW_ALL, retainedFilter
|
||||
);
|
||||
1
|
||||
"#
|
||||
))
|
||||
.expect("retained traversal setup");
|
||||
let walker = vm
|
||||
.with_default_context_scope_and_checkpoint_for_test(|scope, _| {
|
||||
let context = scope.get_current_context();
|
||||
let value = context
|
||||
.global(scope)
|
||||
.get(scope, crate::util::v8str(scope, "retainedTraversal").into())
|
||||
.unwrap();
|
||||
let walker = v8::Local::<v8::Object>::try_from(value).unwrap();
|
||||
Ok(v8::Global::new(scope, walker))
|
||||
})
|
||||
.unwrap();
|
||||
let host = vm.context_host_weak_for_test();
|
||||
let isolate = vm.renderer_document_isolate.clone();
|
||||
drop(vm);
|
||||
for _ in 0..3 {
|
||||
isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification());
|
||||
}
|
||||
assert!(
|
||||
host.upgrade().is_some(),
|
||||
"a retained walker keeps native backing alive"
|
||||
);
|
||||
let result = isolate.with_renderer_document_isolate_mut(|isolate| {
|
||||
let scope = std::pin::pin!(v8::HandleScope::new(isolate));
|
||||
let scope = &mut scope.init();
|
||||
let caller = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, caller);
|
||||
let walker = v8::Local::new(scope, &walker);
|
||||
let context = walker.get_creation_context(scope).unwrap();
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let source = crate::util::v8str(
|
||||
scope,
|
||||
r#"JSON.stringify([
|
||||
retainedTraversal.filter === retainedFilter,
|
||||
retainedTraversal.root.textContent
|
||||
])"#,
|
||||
);
|
||||
let script = v8::Script::compile(scope, source, None).unwrap();
|
||||
crate::script_execution::execute_compiled_script(scope, script)
|
||||
.unwrap()
|
||||
.to_rust_string_lossy(scope)
|
||||
});
|
||||
assert_eq!(result, r#"[true,"old document"]"#);
|
||||
isolate.with_renderer_document_isolate_mut(|isolate| {
|
||||
drop(walker);
|
||||
isolate.low_memory_notification();
|
||||
});
|
||||
for _ in 0..3 {
|
||||
isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification());
|
||||
}
|
||||
assert!(
|
||||
host.upgrade().is_none(),
|
||||
"the last walker reference releases its retired host"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,6 +613,95 @@ fn native_dom_exception_uses_registered_prototype_after_public_override() {
|
||||
.expect("native exception should use the realm registry");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_dom_exception_materializes_without_reading_the_public_constructor() {
|
||||
let mut vm = new_storage_test_vm("https://native-dom-exception-lazy.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.exceptionConstructorReads = 0;
|
||||
delete globalThis.DOMException;
|
||||
Object.defineProperty(globalThis, 'DOMException', {
|
||||
configurable: true,
|
||||
get() {
|
||||
++exceptionConstructorReads;
|
||||
throw new Error('the public constructor must not be read');
|
||||
}
|
||||
});
|
||||
"#,
|
||||
)
|
||||
.expect("public constructor override should evaluate");
|
||||
assert_eq!(
|
||||
constructor_materialization_count(&mut vm, "DOMException"),
|
||||
0
|
||||
);
|
||||
|
||||
vm.with_default_context_scope_and_checkpoint_for_test(|scope, _host_ptr| {
|
||||
let exception = crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
"native security failure",
|
||||
"SecurityError",
|
||||
);
|
||||
let exception = v8::Local::<v8::Object>::try_from(exception).unwrap();
|
||||
let intrinsic =
|
||||
crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "DOMException")?;
|
||||
assert!(
|
||||
exception
|
||||
.get_prototype(scope)
|
||||
.unwrap()
|
||||
.strict_equals(intrinsic.into())
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.expect("native exception should materialize through its realm registry");
|
||||
assert_eq!(vm.eval("exceptionConstructorReads").unwrap(), "0");
|
||||
assert_eq!(
|
||||
constructor_materialization_count(&mut vm, "DOMException"),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_dom_exception_materializes_in_a_detached_realm() {
|
||||
let mut vm = new_storage_test_vm("https://native-dom-exception-detached.test/");
|
||||
assert_eq!(
|
||||
constructor_materialization_count(&mut vm, "DOMException"),
|
||||
0
|
||||
);
|
||||
let isolate = vm.renderer_document_isolate.clone();
|
||||
let context = vm.page_default_context.clone();
|
||||
drop(vm);
|
||||
isolate.with_renderer_document_isolate_mut(|isolate| {
|
||||
let scope = std::pin::pin!(v8::HandleScope::new(isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = v8::Local::new(scope, &context);
|
||||
context.detach_global();
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let exception = crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
"native security failure",
|
||||
"SecurityError",
|
||||
);
|
||||
let exception = v8::Local::<v8::Object>::try_from(exception).unwrap();
|
||||
let intrinsic =
|
||||
crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "DOMException")
|
||||
.expect("detached realm must retain its intrinsic registry");
|
||||
assert!(
|
||||
exception
|
||||
.get_prototype(scope)
|
||||
.unwrap()
|
||||
.strict_equals(intrinsic.into())
|
||||
);
|
||||
assert_eq!(exception.get_creation_context(scope), Some(context));
|
||||
assert_eq!(
|
||||
exception
|
||||
.get(scope, crate::util::v8str(scope, "name").into())
|
||||
.unwrap()
|
||||
.to_rust_string_lossy(scope),
|
||||
"SecurityError"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illegal_storage_receivers_do_not_materialize_wrappers_or_opfs_state() {
|
||||
let mut vm = new_storage_test_vm("https://lazy-illegal-storage-receivers.test/");
|
||||
|
||||
+226
-10
@@ -18,7 +18,11 @@ pub use moli_v8_util::{
|
||||
};
|
||||
use moli_webapi_declare::WebApiValue;
|
||||
pub(crate) use moli_webapi_declare::define_array_data_property as define_v8_array_data_property;
|
||||
use std::{ptr::NonNull, rc::Rc};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
ptr::NonNull,
|
||||
rc::Rc,
|
||||
};
|
||||
use url::Url;
|
||||
use widestring::U16String;
|
||||
|
||||
@@ -26,10 +30,113 @@ const SCRIPT_BASE_URL_HOST_DEFINED_OPTIONS_MARKER: &str = "moli-script-base-url-
|
||||
|
||||
// Access-check callbacks cannot inspect the global object to rediscover the host:
|
||||
// that property lookup would recursively invoke the same V8 access check. This
|
||||
// non-owning slot is valid while the context can execute because the matching
|
||||
// ScriptVm context state retains its `JsContextHostBridgeRef`.
|
||||
// slot starts non-owning during fallible bootstrap, then a successfully
|
||||
// published Document realm promotes it to a Context-owned host reference. That
|
||||
// mirrors ScriptState/Oilpan's retained-realm edge closely enough for Moli's
|
||||
// non-tracing Rust heap: a retained function or DOM wrapper keeps its native
|
||||
// Document backing alive, while temporary WindowProxy facades remain
|
||||
// non-owning and fail closed as soon as the host detaches.
|
||||
struct ContextHostPointerSlot {
|
||||
host_ptr: NonNull<JsContextHost>,
|
||||
host_lifecycle: Rc<Cell<ContextHostLifecycle>>,
|
||||
retained_realm_owner: RefCell<Option<RetainedDocumentRealmHostOwner>>,
|
||||
}
|
||||
|
||||
struct RetainedDocumentRealmHostOwner {
|
||||
host: Option<Rc<RefCell<JsContextHost>>>,
|
||||
deferred_release_queue: crate::script_vm::RendererDeferredContextHostReleaseQueue,
|
||||
retained_v8_handle_state: Vec<Box<dyn std::any::Any>>,
|
||||
}
|
||||
|
||||
impl Drop for RetainedDocumentRealmHostOwner {
|
||||
fn drop(&mut self) {
|
||||
if let Some(host) = self.host.take() {
|
||||
self.deferred_release_queue
|
||||
.defer(host, std::mem::take(&mut self.retained_v8_handle_state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ContextHostLifecycle {
|
||||
Active,
|
||||
Detached,
|
||||
Destroyed,
|
||||
}
|
||||
|
||||
/// Marks a V8 Context whose native Document host has been retired.
|
||||
///
|
||||
/// Navigation can keep the stable WindowProxy while replacing its Context and
|
||||
/// `JsContextHost`; final Page close also parks that proxy on a host-free
|
||||
/// facade. In both cases retained functions and wrappers can outlive the Rust
|
||||
/// host, so every raw-pointer entry point must fail closed before inspecting
|
||||
/// embedder data.
|
||||
#[derive(Debug)]
|
||||
struct ContextHostPointerSlot(NonNull<JsContextHost>);
|
||||
struct DisconnectedPageContext;
|
||||
|
||||
/// Marks a real Document realm whose browsing-context execution authority has
|
||||
/// ended while its ordinary JS and DOM objects remain usable if author code
|
||||
/// still retains them.
|
||||
#[derive(Debug)]
|
||||
struct DetachedDocumentPageContext;
|
||||
|
||||
/// Active native registries root their objects until teardown. A detached
|
||||
/// realm instead keeps those objects in its own V8-traced graph, so retaining
|
||||
/// a Document preserves wrapper identity without a Rust -> V8 -> Rust cycle.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RealmObjectHandle {
|
||||
Strong(v8::Global<v8::Object>),
|
||||
Traced(v8::Weak<v8::Object>),
|
||||
}
|
||||
|
||||
impl RealmObjectHandle {
|
||||
pub(crate) fn new(scope: &mut v8::PinScope<'_, '_>, object: v8::Local<'_, v8::Object>) -> Self {
|
||||
if page_context_is_detached_document(scope.get_current_context()) {
|
||||
Self::traced(scope, object)
|
||||
} else {
|
||||
Self::Strong(v8::Global::new(scope, object))
|
||||
}
|
||||
}
|
||||
|
||||
fn traced(scope: &mut v8::PinScope<'_, '_>, object: v8::Local<'_, v8::Object>) -> Self {
|
||||
const ROOTS: &str = "__moliRetainedRealmObjects";
|
||||
let context = object
|
||||
.get_creation_context(scope)
|
||||
.unwrap_or_else(|| scope.get_current_context());
|
||||
let anchor = context.get_extras_binding_object(scope);
|
||||
let roots = get_private_value(scope, anchor, ROOTS)
|
||||
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
|
||||
.unwrap_or_else(|| {
|
||||
let roots = v8::Array::new(scope, 0);
|
||||
roots.set_prototype(scope, v8::null(scope).into());
|
||||
set_private_value(scope, anchor, ROOTS, roots.into());
|
||||
roots
|
||||
});
|
||||
roots.set_index(scope, roots.length(), object.into());
|
||||
Self::Traced(v8::Weak::new(scope, object))
|
||||
}
|
||||
|
||||
pub(crate) fn to_local<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
match self {
|
||||
Self::Strong(object) => Some(v8::Local::new(scope, object)),
|
||||
Self::Traced(object) => object.to_local(scope),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn retain_in_realm(&mut self, scope: &mut v8::PinScope<'_, '_>) {
|
||||
if let Self::Strong(object) = self {
|
||||
let object = v8::Local::new(scope, &*object);
|
||||
*self = Self::traced(scope, object);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_strong(&self) -> bool {
|
||||
matches!(self, Self::Strong(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes the unexposed prototype described by a WebIDL iterator
|
||||
/// FunctionTemplate. The temporary constructor is an implementation detail, so
|
||||
@@ -373,6 +480,9 @@ pub(super) fn context_host_ptr_from_global_bridge(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> Option<*mut JsContextHost> {
|
||||
let context = scope.get_current_context();
|
||||
if page_context_is_disconnected(context) {
|
||||
return None;
|
||||
}
|
||||
if let Some(host_ptr) = context_host_ptr_from_context_slot(context) {
|
||||
return Some(host_ptr);
|
||||
}
|
||||
@@ -384,24 +494,123 @@ pub(super) fn context_host_ptr_from_global_bridge(
|
||||
pub(super) fn install_context_host_pointer_slot(
|
||||
context: v8::Local<'_, v8::Context>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
host_lifecycle: Rc<Cell<ContextHostLifecycle>>,
|
||||
) {
|
||||
let host_ptr =
|
||||
NonNull::new(host_ptr).expect("V8 context JsContextHost pointer should not be null");
|
||||
let previous = context.set_slot(Rc::new(ContextHostPointerSlot(host_ptr)));
|
||||
assert!(
|
||||
previous
|
||||
.as_deref()
|
||||
.is_none_or(|previous| previous.0 == host_ptr),
|
||||
"V8 context JsContextHost pointer must not be rebound"
|
||||
host_lifecycle.get() == ContextHostLifecycle::Active,
|
||||
"a retired JsContextHost must not be installed into a V8 context"
|
||||
);
|
||||
if let Some(existing) = context.get_slot::<ContextHostPointerSlot>() {
|
||||
assert!(
|
||||
existing.host_ptr == host_ptr && Rc::ptr_eq(&existing.host_lifecycle, &host_lifecycle),
|
||||
"V8 context JsContextHost pointer must not be rebound"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let previous = context.set_slot(Rc::new(ContextHostPointerSlot {
|
||||
host_ptr,
|
||||
host_lifecycle: host_lifecycle.clone(),
|
||||
retained_realm_owner: RefCell::new(None),
|
||||
}));
|
||||
debug_assert!(previous.is_none());
|
||||
}
|
||||
|
||||
pub(super) fn context_host_ptr_from_context_slot(
|
||||
context: v8::Local<'_, v8::Context>,
|
||||
) -> Option<*mut JsContextHost> {
|
||||
if page_context_is_disconnected(context) {
|
||||
return None;
|
||||
}
|
||||
context
|
||||
.get_slot::<ContextHostPointerSlot>()
|
||||
.map(|slot| slot.0.as_ptr())
|
||||
.map(|slot| slot.host_ptr.as_ptr())
|
||||
}
|
||||
|
||||
pub(crate) fn retain_context_host_for_document_realm(
|
||||
context: v8::Local<'_, v8::Context>,
|
||||
host: Rc<RefCell<JsContextHost>>,
|
||||
deferred_release_queue: crate::script_vm::RendererDeferredContextHostReleaseQueue,
|
||||
) {
|
||||
let slot = context
|
||||
.get_slot::<ContextHostPointerSlot>()
|
||||
.expect("published Document realm must have a JsContextHost pointer slot");
|
||||
let host_ptr = NonNull::new(host.as_ptr())
|
||||
.expect("published Document realm JsContextHost pointer should not be null");
|
||||
assert_eq!(
|
||||
slot.host_ptr, host_ptr,
|
||||
"published Document realm must retain its installed JsContextHost"
|
||||
);
|
||||
assert!(
|
||||
slot.host_lifecycle.get() != ContextHostLifecycle::Destroyed,
|
||||
"destroyed JsContextHost must not gain a retained realm owner"
|
||||
);
|
||||
let mut retained = slot.retained_realm_owner.borrow_mut();
|
||||
if let Some(existing) = retained.as_ref() {
|
||||
assert!(
|
||||
existing
|
||||
.host
|
||||
.as_ref()
|
||||
.is_some_and(|existing| Rc::ptr_eq(existing, &host)),
|
||||
"Document realm JsContextHost owner must not be rebound"
|
||||
);
|
||||
return;
|
||||
}
|
||||
*retained = Some(RetainedDocumentRealmHostOwner {
|
||||
host: Some(host),
|
||||
deferred_release_queue,
|
||||
retained_v8_handle_state: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn retain_context_v8_handle_state_for_safe_release<T: 'static>(
|
||||
context: v8::Local<'_, v8::Context>,
|
||||
state: Rc<T>,
|
||||
) {
|
||||
let slot = context
|
||||
.get_slot::<ContextHostPointerSlot>()
|
||||
.expect("retained Context embedder state requires a Document host slot");
|
||||
let mut retained_owner = slot.retained_realm_owner.borrow_mut();
|
||||
let retained_owner = retained_owner
|
||||
.as_mut()
|
||||
.expect("retained Context embedder state requires a published real realm");
|
||||
retained_owner
|
||||
.retained_v8_handle_state
|
||||
.push(Box::new(state));
|
||||
}
|
||||
|
||||
pub(crate) fn page_context_is_disconnected(context: v8::Local<'_, v8::Context>) -> bool {
|
||||
context.get_slot::<DisconnectedPageContext>().is_some()
|
||||
|| context
|
||||
.get_slot::<ContextHostPointerSlot>()
|
||||
.is_some_and(|slot| match slot.host_lifecycle.get() {
|
||||
ContextHostLifecycle::Active => false,
|
||||
ContextHostLifecycle::Detached => slot.retained_realm_owner.borrow().is_none(),
|
||||
ContextHostLifecycle::Destroyed => true,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn page_context_is_detached_document(context: v8::Local<'_, v8::Context>) -> bool {
|
||||
context.get_slot::<DetachedDocumentPageContext>().is_some()
|
||||
|| context
|
||||
.get_slot::<ContextHostPointerSlot>()
|
||||
.is_some_and(|slot| slot.host_lifecycle.get() == ContextHostLifecycle::Detached)
|
||||
}
|
||||
|
||||
pub(crate) fn detach_document_page_context(context: v8::Local<'_, v8::Context>) {
|
||||
if page_context_is_disconnected(context) || page_context_is_detached_document(context) {
|
||||
return;
|
||||
}
|
||||
let slot = context
|
||||
.get_slot::<ContextHostPointerSlot>()
|
||||
.expect("detached Document realm must have a JsContextHost pointer slot");
|
||||
assert!(
|
||||
slot.retained_realm_owner.borrow().is_some(),
|
||||
"only a retained real Document realm may enter detached state"
|
||||
);
|
||||
let previous = context.set_slot(Rc::new(DetachedDocumentPageContext));
|
||||
debug_assert!(previous.is_none());
|
||||
}
|
||||
|
||||
/// Convenience wrapper for callbacks that immediately need the host value.
|
||||
@@ -445,6 +654,13 @@ pub(super) fn context_host_ptr_from_window_object(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
object: v8::Local<'_, v8::Object>,
|
||||
) -> Option<*mut JsContextHost> {
|
||||
if page_context_is_disconnected(scope.get_current_context())
|
||||
|| object
|
||||
.get_creation_context(scope)
|
||||
.is_some_and(page_context_is_disconnected)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if let Some(value) = object.get_internal_field(scope, 0)
|
||||
&& let Ok(external) = v8::Local::<v8::External>::try_from(value)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! V8-traced residence for a Web IDL callback function.
|
||||
//! V8-traced residence for Web IDL callback functions and interfaces.
|
||||
//!
|
||||
//! Some Web API owners already live as JavaScript objects. Storing their
|
||||
//! callbacks in independent Rust `Global` roots would keep callback-to-owner
|
||||
@@ -11,13 +11,88 @@
|
||||
//! Owners that need exact Window currentness must add that authorization before
|
||||
//! invoking the prepared callback.
|
||||
|
||||
use moli_webidl_callback::{PreparedWebIdlCallbackFunction, WebIdlCallbackFunction};
|
||||
use moli_webidl_callback::{
|
||||
PreparedWebIdlCallbackFunction, PreparedWebIdlCallbackInterface, WebIdlCallbackFunction,
|
||||
WebIdlCallbackInterface,
|
||||
};
|
||||
|
||||
use crate::util::{get_private_value, set_private_value};
|
||||
|
||||
const CALLBACK_VALUE_SLOT: &str = "__lmV8TracedWebIdlCallbackValue";
|
||||
const CALLBACK_RELEVANT_GLOBAL_SLOT: &str = "__lmV8TracedWebIdlCallbackRelevantGlobal";
|
||||
const CALLBACK_INCUMBENT_GLOBAL_SLOT: &str = "__lmV8TracedWebIdlCallbackIncumbentGlobal";
|
||||
const CALLBACK_CALLABLE_SLOT: &str = "__lmV8TracedWebIdlCallbackCallableAtConversion";
|
||||
|
||||
/// Callback-interface residence owned by a JavaScript wrapper. Context extras
|
||||
/// retain the original realms even after their WindowProxies are detached.
|
||||
pub(crate) struct V8TracedWebIdlCallbackInterface<'s> {
|
||||
carrier: v8::Local<'s, v8::Object>,
|
||||
}
|
||||
|
||||
impl<'s> V8TracedWebIdlCallbackInterface<'s> {
|
||||
pub(crate) fn new(scope: &mut v8::PinScope<'s, '_>, callback: WebIdlCallbackInterface) -> Self {
|
||||
let carrier = v8::Object::new(scope);
|
||||
let value = callback.value(scope);
|
||||
let relevant = callback
|
||||
.relevant_context(scope)
|
||||
.get_extras_binding_object(scope);
|
||||
let incumbent = callback
|
||||
.incumbent_context(scope)
|
||||
.get_extras_binding_object(scope);
|
||||
let callable = v8::Boolean::new(scope, callback.callable_at_conversion());
|
||||
set_private_value(scope, carrier, CALLBACK_VALUE_SLOT, value);
|
||||
set_private_value(
|
||||
scope,
|
||||
carrier,
|
||||
CALLBACK_RELEVANT_GLOBAL_SLOT,
|
||||
relevant.into(),
|
||||
);
|
||||
set_private_value(
|
||||
scope,
|
||||
carrier,
|
||||
CALLBACK_INCUMBENT_GLOBAL_SLOT,
|
||||
incumbent.into(),
|
||||
);
|
||||
set_private_value(scope, carrier, CALLBACK_CALLABLE_SLOT, callable.into());
|
||||
Self { carrier }
|
||||
}
|
||||
|
||||
pub(crate) const fn from_object(carrier: v8::Local<'s, v8::Object>) -> Self {
|
||||
Self { carrier }
|
||||
}
|
||||
|
||||
pub(crate) const fn into_object(self) -> v8::Local<'s, v8::Object> {
|
||||
self.carrier
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> PreparedWebIdlCallbackInterface {
|
||||
let object = |scope: &mut v8::PinScope<'s, '_>, slot| {
|
||||
get_private_value(scope, self.carrier, slot)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
.expect("a traced callback interface must retain its registration facts")
|
||||
};
|
||||
let callback = object(scope, CALLBACK_VALUE_SLOT);
|
||||
let relevant_context = object(scope, CALLBACK_RELEVANT_GLOBAL_SLOT)
|
||||
.get_creation_context(scope)
|
||||
.expect("callback relevant realm anchor");
|
||||
let incumbent_context = object(scope, CALLBACK_INCUMBENT_GLOBAL_SLOT)
|
||||
.get_creation_context(scope)
|
||||
.expect("callback incumbent realm anchor");
|
||||
let callable = get_private_value(scope, self.carrier, CALLBACK_CALLABLE_SLOT)
|
||||
.expect("callback-interface callability at conversion")
|
||||
.is_true();
|
||||
PreparedWebIdlCallbackInterface::new(
|
||||
scope,
|
||||
callback,
|
||||
relevant_context,
|
||||
incumbent_context,
|
||||
callable,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct V8TracedWebIdlCallbackFunction<'s> {
|
||||
carrier: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -32,15 +32,13 @@ impl WebIdlCallbackInterface {
|
||||
/// Event dispatch can therefore release all registration-store borrows
|
||||
/// before entering user code and its synchronous reentrancy.
|
||||
pub fn prepare(&self, scope: &mut v8::PinScope<'_, '_>) -> PreparedWebIdlCallbackInterface {
|
||||
PreparedWebIdlCallbackInterface {
|
||||
callback: v8::Global::new(scope, v8::Local::new(scope, &self.callback)),
|
||||
relevant_context: v8::Global::new(scope, v8::Local::new(scope, &self.relevant_context)),
|
||||
incumbent_context: v8::Global::new(
|
||||
scope,
|
||||
v8::Local::new(scope, &self.incumbent_context),
|
||||
),
|
||||
callable_at_conversion: self.callable_at_conversion,
|
||||
}
|
||||
PreparedWebIdlCallbackInterface::new(
|
||||
scope,
|
||||
v8::Local::new(scope, &self.callback),
|
||||
v8::Local::new(scope, &self.relevant_context),
|
||||
v8::Local::new(scope, &self.incumbent_context),
|
||||
self.callable_at_conversion,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn matches<'s>(
|
||||
@@ -83,6 +81,22 @@ pub struct PreparedWebIdlCallbackInterface {
|
||||
}
|
||||
|
||||
impl PreparedWebIdlCallbackInterface {
|
||||
/// Root a snapshot of callback-interface facts held by a traced owner.
|
||||
pub fn new(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
callback: v8::Local<'_, v8::Object>,
|
||||
relevant_context: v8::Local<'_, v8::Context>,
|
||||
incumbent_context: v8::Local<'_, v8::Context>,
|
||||
callable_at_conversion: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
callback: v8::Global::new(scope, callback),
|
||||
relevant_context: v8::Global::new(scope, relevant_context),
|
||||
incumbent_context: v8::Global::new(scope, incumbent_context),
|
||||
callable_at_conversion,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn callback<'s>(&self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> {
|
||||
v8::Local::new(scope, &self.callback)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user