/* 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, } /// Private helper to enable adding new methods to `Rc`. trait PromiseHelper { fn initialize(&self, cx: SafeJSContext); } impl PromiseHelper for Rc { #[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 { 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 { let cx = GlobalScope::get_cx(); rooted!(in(*cx) let mut obj = ptr::null_mut::()); 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 { 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 { 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 { 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 { 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(&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(&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::(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>, ()> { 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 type WaitForAllSuccessSteps = Rc)>; /// The failure steps of type WaitForAllFailureSteps = Rc; /// The fulfillment handler for the list of promises in /// . #[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>>>>, /// 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>, } 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 = 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 /// . #[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, } 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); } } /// pub(crate) fn wait_for_all( cx: SafeJSContext, global: &GlobalScope, promises: Vec>, success_steps: WaitForAllSuccessSteps, failure_steps: WaitForAllFailureSteps, realm: InRealm, can_gc: CanGc, ) { // Let fulfilledCount be 0. let fulfilled_count: Rc> = 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 promises’s 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>>>> = 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`. } } /// pub(crate) fn wait_for_all_promise( cx: SafeJSContext, global: &GlobalScope, promises: Vec>, realm: InRealm, can_gc: CanGc, ) -> Rc { // Let promise be a new promise of type Promise> 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| { // 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 = 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 }