servo/components/script/dom/promise.rs
Domenico Rizzo b8738074d1
[#26488] Refactored RTCDataChannel for safer dropping and added Promise comment (#37332)
Fixes (partially) #26488 and apply the
https://github.com/servo/servo/pull/37324#discussion_r2133989190
comment.

Testing: No tests added
Fixes: Partially #26488

---------

Signed-off-by: Domenico Rizzo <domenico.rizzo@gmail.com>
2025-06-13 12:20:45 +00:00

629 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Native representation of JS Promise values.
//!
//! This implementation differs from the traditional Rust DOM object, because the reflector
//! is provided by SpiderMonkey and has no knowledge of an associated native representation
//! (ie. dom::Promise). This means that native instances use native reference counting (Rc)
//! to ensure that no memory is leaked, which means that there can be multiple instances of
//! native Promise values that refer to the same JS value yet are distinct native objects
//! (ie. address equality for the native objects is meaningless).
use std::cell::{Cell, RefCell};
use std::ptr;
use std::rc::Rc;
use dom_struct::dom_struct;
use js::conversions::{ConversionResult, FromJSValConvertibleRc, ToJSValConvertible};
use js::jsapi::{
AddRawValueRoot, CallArgs, GetFunctionNativeReserved, Heap, JS_ClearPendingException,
JS_GetFunctionObject, JS_NewFunction, JSAutoRealm, JSContext, JSObject,
NewFunctionWithReserved, PromiseState, PromiseUserInputEventHandlingState, RemoveRawValueRoot,
SetFunctionNativeReserved,
};
use js::jsval::{Int32Value, JSVal, NullValue, ObjectValue, UndefinedValue};
use js::rust::wrappers::{
AddPromiseReactions, CallOriginalPromiseReject, CallOriginalPromiseResolve,
GetPromiseIsHandled, GetPromiseState, IsPromiseObject, NewPromiseObject, RejectPromise,
ResolvePromise, SetAnyPromiseIsHandled, SetPromiseUserInputEventHandlingState,
};
use js::rust::{HandleObject, HandleValue, MutableHandleObject, Runtime};
use crate::dom::bindings::conversions::root_from_object;
use crate::dom::bindings::error::{Error, ErrorToJsval};
use crate::dom::bindings::reflector::{DomGlobal, DomObject, MutDomObject, Reflector};
use crate::dom::bindings::settings_stack::AutoEntryScript;
use crate::dom::globalscope::GlobalScope;
use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
use crate::realms::{AlreadyInRealm, InRealm, enter_realm};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
use crate::script_thread::ScriptThread;
#[dom_struct]
#[cfg_attr(crown, crown::unrooted_must_root_lint::allow_unrooted_in_rc)]
pub(crate) struct Promise {
reflector: Reflector,
/// Since Promise values are natively reference counted without the knowledge of
/// the SpiderMonkey GC, an explicit root for the reflector is stored while any
/// native instance exists. This ensures that the reflector will never be GCed
/// while native code could still interact with its native representation.
#[ignore_malloc_size_of = "SM handles JS values"]
permanent_js_root: Heap<JSVal>,
}
/// Private helper to enable adding new methods to `Rc<Promise>`.
trait PromiseHelper {
fn initialize(&self, cx: SafeJSContext);
}
impl PromiseHelper for Rc<Promise> {
#[allow(unsafe_code)]
fn initialize(&self, cx: SafeJSContext) {
let obj = self.reflector().get_jsobject();
self.permanent_js_root.set(ObjectValue(*obj));
unsafe {
assert!(AddRawValueRoot(
*cx,
self.permanent_js_root.get_unsafe(),
c"Promise::root".as_ptr(),
));
}
}
}
// Promise objects are stored inside Rc values, so Drop is run when the last Rc is dropped,
// rather than when SpiderMonkey runs a GC. This makes it safe to interact with the JS engine unlike
// Drop implementations for other DOM types.
impl Drop for Promise {
#[allow(unsafe_code)]
fn drop(&mut self) {
unsafe {
let object = self.permanent_js_root.get().to_object();
assert!(!object.is_null());
if let Some(cx) = Runtime::get() {
RemoveRawValueRoot(cx.as_ptr(), self.permanent_js_root.get_unsafe());
}
}
}
}
impl Promise {
pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> Rc<Promise> {
let realm = enter_realm(global);
let comp = InRealm::Entered(&realm);
Promise::new_in_current_realm(comp, can_gc)
}
pub(crate) fn new_in_current_realm(_comp: InRealm, can_gc: CanGc) -> Rc<Promise> {
let cx = GlobalScope::get_cx();
rooted!(in(*cx) let mut obj = ptr::null_mut::<JSObject>());
Promise::create_js_promise(cx, obj.handle_mut(), can_gc);
Promise::new_with_js_promise(obj.handle(), cx)
}
#[allow(unsafe_code)]
pub(crate) fn duplicate(&self) -> Rc<Promise> {
let cx = GlobalScope::get_cx();
Promise::new_with_js_promise(self.reflector().get_jsobject(), cx)
}
#[allow(unsafe_code)]
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new_with_js_promise(obj: HandleObject, cx: SafeJSContext) -> Rc<Promise> {
unsafe {
assert!(IsPromiseObject(obj));
let promise = Promise {
reflector: Reflector::new(),
permanent_js_root: Heap::default(),
};
let promise = Rc::new(promise);
promise.init_reflector(obj.get());
promise.initialize(cx);
promise
}
}
#[allow(unsafe_code)]
// The apparently-unused CanGc parameter reflects the fact that the JS API calls
// like JS_NewFunction can trigger a GC.
fn create_js_promise(cx: SafeJSContext, mut obj: MutableHandleObject, _can_gc: CanGc) {
unsafe {
let do_nothing_func = JS_NewFunction(
*cx,
Some(do_nothing_promise_executor),
/* nargs = */ 2,
/* flags = */ 0,
ptr::null(),
);
assert!(!do_nothing_func.is_null());
rooted!(in(*cx) let do_nothing_obj = JS_GetFunctionObject(do_nothing_func));
assert!(!do_nothing_obj.is_null());
obj.set(NewPromiseObject(*cx, do_nothing_obj.handle()));
assert!(!obj.is_null());
let is_user_interacting = if ScriptThread::is_user_interacting() {
PromiseUserInputEventHandlingState::HadUserInteractionAtCreation
} else {
PromiseUserInputEventHandlingState::DidntHaveUserInteractionAtCreation
};
SetPromiseUserInputEventHandlingState(obj.handle(), is_user_interacting);
}
}
#[allow(unsafe_code)]
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new_resolved(
global: &GlobalScope,
cx: SafeJSContext,
value: impl ToJSValConvertible,
_can_gc: CanGc,
) -> Rc<Promise> {
let _ac = JSAutoRealm::new(*cx, global.reflector().get_jsobject().get());
unsafe {
rooted!(in(*cx) let mut rval = UndefinedValue());
value.to_jsval(*cx, rval.handle_mut());
rooted!(in(*cx) let p = CallOriginalPromiseResolve(*cx, rval.handle()));
assert!(!p.handle().is_null());
Promise::new_with_js_promise(p.handle(), cx)
}
}
#[allow(unsafe_code)]
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new_rejected(
global: &GlobalScope,
cx: SafeJSContext,
value: impl ToJSValConvertible,
_can_gc: CanGc,
) -> Rc<Promise> {
let _ac = JSAutoRealm::new(*cx, global.reflector().get_jsobject().get());
unsafe {
rooted!(in(*cx) let mut rval = UndefinedValue());
value.to_jsval(*cx, rval.handle_mut());
rooted!(in(*cx) let p = CallOriginalPromiseReject(*cx, rval.handle()));
assert!(!p.handle().is_null());
Promise::new_with_js_promise(p.handle(), cx)
}
}
#[allow(unsafe_code)]
pub(crate) fn resolve_native<T>(&self, val: &T, can_gc: CanGc)
where
T: ToJSValConvertible,
{
let cx = GlobalScope::get_cx();
let _ac = enter_realm(self);
rooted!(in(*cx) let mut v = UndefinedValue());
unsafe {
val.to_jsval(*cx, v.handle_mut());
}
self.resolve(cx, v.handle(), can_gc);
}
#[allow(unsafe_code)]
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn resolve(&self, cx: SafeJSContext, value: HandleValue, _can_gc: CanGc) {
unsafe {
if !ResolvePromise(*cx, self.promise_obj(), value) {
JS_ClearPendingException(*cx);
}
}
}
#[allow(unsafe_code)]
pub(crate) fn reject_native<T>(&self, val: &T, can_gc: CanGc)
where
T: ToJSValConvertible,
{
let cx = GlobalScope::get_cx();
let _ac = enter_realm(self);
rooted!(in(*cx) let mut v = UndefinedValue());
unsafe {
val.to_jsval(*cx, v.handle_mut());
}
self.reject(cx, v.handle(), can_gc);
}
pub(crate) fn reject_error(&self, error: Error, can_gc: CanGc) {
let cx = GlobalScope::get_cx();
let _ac = enter_realm(self);
rooted!(in(*cx) let mut v = UndefinedValue());
error.to_jsval(cx, &self.global(), v.handle_mut(), can_gc);
self.reject(cx, v.handle(), can_gc);
}
#[allow(unsafe_code)]
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn reject(&self, cx: SafeJSContext, value: HandleValue, _can_gc: CanGc) {
unsafe {
if !RejectPromise(*cx, self.promise_obj(), value) {
JS_ClearPendingException(*cx);
}
}
}
#[allow(unsafe_code)]
pub(crate) fn is_fulfilled(&self) -> bool {
let state = unsafe { GetPromiseState(self.promise_obj()) };
matches!(state, PromiseState::Rejected | PromiseState::Fulfilled)
}
#[allow(unsafe_code)]
pub(crate) fn is_rejected(&self) -> bool {
let state = unsafe { GetPromiseState(self.promise_obj()) };
matches!(state, PromiseState::Rejected)
}
#[allow(unsafe_code)]
pub(crate) fn is_pending(&self) -> bool {
let state = unsafe { GetPromiseState(self.promise_obj()) };
matches!(state, PromiseState::Pending)
}
#[allow(unsafe_code)]
pub(crate) fn promise_obj(&self) -> HandleObject {
let obj = self.reflector().get_jsobject();
unsafe {
assert!(IsPromiseObject(obj));
}
obj
}
#[allow(unsafe_code)]
pub(crate) fn append_native_handler(
&self,
handler: &PromiseNativeHandler,
realm: InRealm,
can_gc: CanGc,
) {
let _ais = AutoEntryScript::new(&handler.global_(realm));
let cx = GlobalScope::get_cx();
rooted!(in(*cx) let resolve_func =
create_native_handler_function(*cx,
handler.reflector().get_jsobject(),
NativeHandlerTask::Resolve,
can_gc));
rooted!(in(*cx) let reject_func =
create_native_handler_function(*cx,
handler.reflector().get_jsobject(),
NativeHandlerTask::Reject,
can_gc));
unsafe {
let ok = AddPromiseReactions(
*cx,
self.promise_obj(),
resolve_func.handle(),
reject_func.handle(),
);
assert!(ok);
}
}
#[allow(unsafe_code)]
pub(crate) fn get_promise_is_handled(&self) -> bool {
unsafe { GetPromiseIsHandled(self.reflector().get_jsobject()) }
}
#[allow(unsafe_code)]
pub(crate) fn set_promise_is_handled(&self) -> bool {
let cx = GlobalScope::get_cx();
unsafe { SetAnyPromiseIsHandled(*cx, self.reflector().get_jsobject()) }
}
}
#[allow(unsafe_code)]
unsafe extern "C" fn do_nothing_promise_executor(
_cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, argc);
args.rval().set(UndefinedValue());
true
}
const SLOT_NATIVEHANDLER: usize = 0;
const SLOT_NATIVEHANDLER_TASK: usize = 1;
#[derive(PartialEq)]
enum NativeHandlerTask {
Resolve = 0,
Reject = 1,
}
#[allow(unsafe_code)]
unsafe extern "C" fn native_handler_callback(
cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
let cx = SafeJSContext::from_ptr(cx);
let in_realm_proof = AlreadyInRealm::assert_for_cx(cx);
let args = CallArgs::from_vp(vp, argc);
rooted!(in(*cx) let v = *GetFunctionNativeReserved(args.callee(), SLOT_NATIVEHANDLER));
assert!(v.get().is_object());
let handler = root_from_object::<PromiseNativeHandler>(v.to_object(), *cx)
.expect("unexpected value for native handler in promise native handler callback");
rooted!(in(*cx) let v = *GetFunctionNativeReserved(args.callee(), SLOT_NATIVEHANDLER_TASK));
match v.to_int32() {
v if v == NativeHandlerTask::Resolve as i32 => handler.resolved_callback(
*cx,
HandleValue::from_raw(args.get(0)),
InRealm::Already(&in_realm_proof),
CanGc::note(),
),
v if v == NativeHandlerTask::Reject as i32 => handler.rejected_callback(
*cx,
HandleValue::from_raw(args.get(0)),
InRealm::Already(&in_realm_proof),
CanGc::note(),
),
_ => panic!("unexpected native handler task value"),
};
true
}
#[allow(unsafe_code)]
// The apparently-unused CanGc argument reflects the fact that the JS API calls
// like NewFunctionWithReserved can trigger a GC.
fn create_native_handler_function(
cx: *mut JSContext,
holder: HandleObject,
task: NativeHandlerTask,
_can_gc: CanGc,
) -> *mut JSObject {
unsafe {
let func = NewFunctionWithReserved(cx, Some(native_handler_callback), 1, 0, ptr::null());
assert!(!func.is_null());
rooted!(in(cx) let obj = JS_GetFunctionObject(func));
assert!(!obj.is_null());
SetFunctionNativeReserved(obj.get(), SLOT_NATIVEHANDLER, &ObjectValue(*holder));
SetFunctionNativeReserved(obj.get(), SLOT_NATIVEHANDLER_TASK, &Int32Value(task as i32));
obj.get()
}
}
impl FromJSValConvertibleRc for Promise {
#[allow(unsafe_code)]
unsafe fn from_jsval(
cx: *mut JSContext,
value: HandleValue,
) -> Result<ConversionResult<Rc<Promise>>, ()> {
if value.get().is_null() {
return Ok(ConversionResult::Failure("null not allowed".into()));
}
let cx = SafeJSContext::from_ptr(cx);
let in_realm_proof = AlreadyInRealm::assert_for_cx(cx);
let global_scope = GlobalScope::from_context(*cx, InRealm::Already(&in_realm_proof));
let promise = Promise::new_resolved(&global_scope, cx, value, CanGc::note());
Ok(ConversionResult::Success(promise))
}
}
/// The success steps of <https://webidl.spec.whatwg.org/#wait-for-all>
type WaitForAllSuccessSteps = Rc<dyn Fn(Vec<HandleValue>)>;
/// The failure steps of <https://webidl.spec.whatwg.org/#wait-for-all>
type WaitForAllFailureSteps = Rc<dyn Fn(HandleValue)>;
/// The fulfillment handler for the list of promises in
/// <https://webidl.spec.whatwg.org/#wait-for-all>.
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct WaitForAllFulfillmentHandler {
/// The steps to call when all promises are resolved.
#[ignore_malloc_size_of = "Rc is hard"]
#[no_trace]
success_steps: WaitForAllSuccessSteps,
/// The results of the promises.
#[ignore_malloc_size_of = "Rc is hard"]
#[allow(clippy::vec_box)]
result: Rc<RefCell<Vec<Box<Heap<JSVal>>>>>,
/// The index identifying which promise this handler is attached to.
promise_index: usize,
/// A count of fulfilled promises.
#[ignore_malloc_size_of = "Rc is hard"]
fulfilled_count: Rc<RefCell<usize>>,
}
impl Callback for WaitForAllFulfillmentHandler {
#[allow(unsafe_code)]
fn callback(&self, _cx: SafeJSContext, v: HandleValue, _realm: InRealm, _can_gc: CanGc) {
// Let fulfillmentHandler be the following steps given arg:
let equals_total = {
// Set result[promiseIndex] to arg.
let result = self.result.borrow_mut();
result[self.promise_index].set(v.get());
// Set fulfilledCount to fulfilledCount + 1.
let mut fulfilled_count = self.fulfilled_count.borrow_mut();
*fulfilled_count += 1;
*fulfilled_count == result.len()
};
// If fulfilledCount equals total, then perform successSteps given result.
if equals_total {
// Safety: the values are kept alive by the Heap
// while their handles are passed to the the success steps.
let result_handles: Vec<HandleValue> = unsafe {
self.result
.borrow()
.iter()
.map(|val| HandleValue::from_raw(val.handle()))
.collect()
};
(self.success_steps)(result_handles);
}
}
}
/// The rejection handler for the list of promises in
/// <https://webidl.spec.whatwg.org/#wait-for-all>.
#[derive(Clone, JSTraceable, MallocSizeOf)]
struct WaitForAllRejectionHandler {
/// The steps to call if any promise rejects.
#[ignore_malloc_size_of = "Rc is hard"]
#[no_trace]
failure_steps: WaitForAllFailureSteps,
/// Whether any promises have been rejected already.
rejected: Cell<bool>,
}
impl Callback for WaitForAllRejectionHandler {
fn callback(&self, _cx: SafeJSContext, v: HandleValue, _realm: InRealm, _can_gc: CanGc) {
// Let rejectionHandlerSteps be the following steps given arg:
if self.rejected.replace(true) {
// If rejected is true, abort these steps.
return;
}
// Set rejected to true.
// Done above with `replace`.
(self.failure_steps)(v);
}
}
/// <https://webidl.spec.whatwg.org/#wait-for-all>
pub(crate) fn wait_for_all(
cx: SafeJSContext,
global: &GlobalScope,
promises: Vec<Rc<Promise>>,
success_steps: WaitForAllSuccessSteps,
failure_steps: WaitForAllFailureSteps,
realm: InRealm,
can_gc: CanGc,
) {
// Let fulfilledCount be 0.
let fulfilled_count: Rc<RefCell<usize>> = Default::default();
// Let rejected be false.
// Note: done below when constructing a rejection handler.
// Let rejectionHandlerSteps be the following steps given arg:
// Note: implemented with the `WaitForAllRejectionHandler`.
// Let rejectionHandler be CreateBuiltinFunction(rejectionHandlerSteps, « »):
// Note: done as part of attaching the `WaitForAllRejectionHandler` as native rejection handler.
let rejection_handler = WaitForAllRejectionHandler {
failure_steps,
rejected: Default::default(),
};
// Let total be promisess size.
// Note: done using the len of result.
// If total is 0, then:
// Queue a microtask to perform successSteps given « ».
// TODO: #37259
// Let index be 0.
// Note: done with `enumerate` below.
// Let result be a list containing total null values.
let result: Rc<RefCell<Vec<Box<Heap<JSVal>>>>> = Default::default();
// For each promise of promises:
for (promise_index, promise) in promises.into_iter().enumerate() {
let result = result.clone();
{
// Note: adding a null value for this promise result.
let mut result_list = result.borrow_mut();
rooted!(in(*cx) let null_value = NullValue());
result_list.push(Heap::boxed(null_value.get()));
}
// Let promiseIndex be index.
// Note: done with `enumerate` above.
// Let fulfillmentHandler be the following steps given arg:
// Note: implemented with the `WaitForAllFulFillmentHandler`.
// Let fulfillmentHandler be CreateBuiltinFunction(fulfillmentHandler, « »):
// Note: passed below to avoid the need to root it.
// Perform PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler).
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(WaitForAllFulfillmentHandler {
success_steps: success_steps.clone(),
result,
promise_index,
fulfilled_count: fulfilled_count.clone(),
})),
Some(Box::new(rejection_handler.clone())),
can_gc,
);
promise.append_native_handler(&handler, realm, can_gc);
// Set index to index + 1.
// Note: done above with `enumerate`.
}
}
/// <https://webidl.spec.whatwg.org/#waiting-for-all-promise>
pub(crate) fn wait_for_all_promise(
cx: SafeJSContext,
global: &GlobalScope,
promises: Vec<Rc<Promise>>,
realm: InRealm,
can_gc: CanGc,
) -> Rc<Promise> {
// Let promise be a new promise of type Promise<sequence<T>> in realm.
let promise = Promise::new(global, can_gc);
let success_promise = promise.clone();
let failure_promise = promise.clone();
// Let successSteps be the following steps, given results:
let success_steps = Rc::new(move |results: Vec<HandleValue>| {
// Resolve promise with results.
success_promise.resolve_native(&results, can_gc);
});
// Let failureSteps be the following steps, given reason:
let failure_steps = Rc::new(move |reason: HandleValue| {
// Reject promise with reason.
failure_promise.reject_native(&reason, can_gc);
});
if promises.is_empty() {
// Note: part of `wait_for_all`.
// Done here by using `resolve_native`.
// TODO: #37259
// If total is 0, then:
// Queue a microtask to perform successSteps given « ».
let empty_list: Vec<HandleValue> = vec![];
promise.resolve_native(&empty_list, can_gc);
} else {
// Wait for all with promises, given successSteps and failureSteps.
wait_for_all(
cx,
global,
promises,
success_steps,
failure_steps,
realm,
can_gc,
);
}
// Return promise.
promise
}