Skip to main content

script/dom/promise/
promise.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Native representation of JS Promise values.
6//!
7//! This implementation differs from the traditional Rust DOM object, because the reflector
8//! is provided by SpiderMonkey and has no knowledge of an associated native representation
9//! (ie. dom::Promise). This means that native instances use native reference counting (Rc)
10//! to ensure that no memory is leaked, which means that there can be multiple instances of
11//! native Promise values that refer to the same JS value yet are distinct native objects
12//! (ie. address equality for the native objects is meaningless).
13
14use std::cell::{Cell, RefCell};
15use std::ops::DerefMut;
16use std::ptr;
17use std::rc::Rc;
18
19use dom_struct::dom_struct;
20use js::context::JSContext;
21use js::conversions::{ConversionResult, FromJSValConvertibleRc, ToJSValConvertible};
22use js::jsapi::{
23    CallArgs, GetFunctionNativeReserved, Heap, JS_GetFunctionObject, JSContext as RawJSContext,
24    JSObject, PromiseState, PromiseUserInputEventHandlingState, RemoveRawValueRoot,
25    SetFunctionNativeReserved,
26};
27use js::jsval::{Int32Value, JSVal, NullValue, ObjectValue, UndefinedValue};
28use js::realm::CurrentRealm;
29use js::rust::wrappers2::{
30    AddPromiseReactions, AddRawValueRoot, CallOriginalPromiseReject, CallOriginalPromiseResolve,
31    GetPromiseIsHandled, GetPromiseState, IsPromiseObject, JS_ClearPendingException,
32    JS_NewFunction, NewFunctionWithReserved, NewPromiseObject, RejectPromise, ResolvePromise,
33    SetAnyPromiseIsHandled, SetPromiseUserInputEventHandlingState,
34};
35use js::rust::{HandleObject, HandleValue, MutableHandleObject, Runtime};
36use script_bindings::reflector::{DomObject, MutDomObject, Reflector};
37use script_bindings::settings_stack::run_a_script;
38
39use crate::DomTypeHolder;
40use crate::dom::bindings::conversions::root_from_object;
41use crate::dom::bindings::error::{Error, ErrorToJsval};
42use crate::dom::bindings::reflector::DomGlobal;
43use crate::dom::bindings::root::{AsHandleValue, Dom};
44use crate::dom::globalscope::GlobalScope;
45use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
46use crate::microtask::MicrotaskRunnable;
47use crate::realms::enter_auto_realm;
48use crate::script_thread::ScriptThread;
49
50#[dom_struct]
51#[cfg_attr(crown, crown::unrooted_must_root_lint::allow_unrooted_in_rc)]
52pub(crate) struct Promise {
53    reflector: Reflector,
54    /// Since Promise values are natively reference counted without the knowledge of
55    /// the SpiderMonkey GC, an explicit root for the reflector is stored while any
56    /// native instance exists. This ensures that the reflector will never be GCed
57    /// while native code could still interact with its native representation.
58    #[ignore_malloc_size_of = "SM handles JS values"]
59    permanent_js_root: Heap<JSVal>,
60}
61
62/// Private helper to enable adding new methods to `Rc<Promise>`.
63trait PromiseHelper {
64    fn initialize(&self, cx: &mut JSContext);
65}
66
67impl PromiseHelper for Rc<Promise> {
68    #[expect(unsafe_code)]
69    fn initialize(&self, cx: &mut JSContext) {
70        let obj = self.reflector().get_jsobject();
71        self.permanent_js_root.set(ObjectValue(*obj));
72        unsafe {
73            assert!(AddRawValueRoot(
74                cx,
75                self.permanent_js_root.get_unsafe(),
76                c"Promise::root".as_ptr(),
77            ));
78        }
79    }
80}
81
82// Promise objects are stored inside Rc values, so Drop is run when the last Rc is dropped,
83// rather than when SpiderMonkey runs a GC. This makes it safe to interact with the JS engine unlike
84// Drop implementations for other DOM types.
85impl Drop for Promise {
86    #[expect(unsafe_code)]
87    fn drop(&mut self) {
88        unsafe {
89            let object = self.permanent_js_root.get().to_object();
90            assert!(!object.is_null());
91            if let Some(cx) = Runtime::get() {
92                RemoveRawValueRoot(cx.as_ptr(), self.permanent_js_root.get_unsafe());
93            }
94        }
95    }
96}
97
98impl Promise {
99    pub(crate) fn new(cx: &mut JSContext, global: &GlobalScope) -> Rc<Promise> {
100        let mut realm = enter_auto_realm(cx, global);
101        let cx = &mut realm.current_realm();
102        Promise::new_in_realm(cx)
103    }
104
105    pub(crate) fn new_in_realm(current_realm: &mut CurrentRealm) -> Rc<Promise> {
106        let cx = current_realm.deref_mut();
107        rooted!(&in(cx) let mut obj = ptr::null_mut::<JSObject>());
108        Promise::create_js_promise(cx, obj.handle_mut());
109        Promise::new_with_js_promise(cx, obj.handle())
110    }
111
112    pub(crate) fn duplicate(&self, cx: &mut JSContext) -> Rc<Promise> {
113        Promise::new_with_js_promise(cx, self.reflector().get_jsobject())
114    }
115
116    #[expect(unsafe_code)]
117    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
118    pub(crate) fn new_with_js_promise(cx: &mut JSContext, obj: HandleObject) -> Rc<Promise> {
119        unsafe {
120            assert!(IsPromiseObject(obj));
121        }
122        let promise = Promise {
123            reflector: Reflector::new(),
124            permanent_js_root: Heap::default(),
125        };
126        let promise = Rc::new(promise);
127        unsafe {
128            promise.init_reflector_without_associated_memory(obj.get());
129        }
130        promise.initialize(cx);
131        promise
132    }
133
134    #[expect(unsafe_code)]
135    fn create_js_promise(cx: &mut JSContext, mut obj: MutableHandleObject) {
136        unsafe {
137            let do_nothing_func = JS_NewFunction(
138                cx,
139                Some(do_nothing_promise_executor),
140                /* nargs = */ 2,
141                /* flags = */ 0,
142                ptr::null(),
143            );
144            assert!(!do_nothing_func.is_null());
145            rooted!(&in(cx) let do_nothing_obj = JS_GetFunctionObject(do_nothing_func));
146            assert!(!do_nothing_obj.is_null());
147            obj.set(NewPromiseObject(cx, do_nothing_obj.handle()));
148            assert!(!obj.is_null());
149            let is_user_interacting = if ScriptThread::is_user_interacting() {
150                PromiseUserInputEventHandlingState::HadUserInteractionAtCreation
151            } else {
152                PromiseUserInputEventHandlingState::DidntHaveUserInteractionAtCreation
153            };
154            SetPromiseUserInputEventHandlingState(obj.handle(), is_user_interacting);
155        }
156    }
157
158    #[expect(unsafe_code)]
159    pub(crate) fn new_resolved(
160        cx: &mut JSContext,
161        global: &GlobalScope,
162        value: impl ToJSValConvertible,
163    ) -> Rc<Promise> {
164        let mut realm = enter_auto_realm(cx, global);
165        let cx = &mut realm.current_realm();
166        rooted!(&in(cx) let mut rval = UndefinedValue());
167        value.safe_to_jsval(cx, rval.handle_mut());
168        rooted!(&in(cx) let p = unsafe { CallOriginalPromiseResolve(cx, rval.handle()) });
169        assert!(!p.handle().is_null());
170        Promise::new_with_js_promise(cx, p.handle())
171    }
172
173    #[expect(unsafe_code)]
174    pub(crate) fn new_rejected(
175        cx: &mut JSContext,
176        global: &GlobalScope,
177        value: impl ToJSValConvertible,
178    ) -> Rc<Promise> {
179        let mut realm = enter_auto_realm(cx, global);
180        let cx = &mut realm.current_realm();
181        rooted!(&in(cx) let mut rval = UndefinedValue());
182        value.safe_to_jsval(cx, rval.handle_mut());
183        rooted!(&in(cx) let p = unsafe { CallOriginalPromiseReject(cx, rval.handle()) });
184        assert!(!p.handle().is_null());
185        Promise::new_with_js_promise(cx, p.handle())
186    }
187
188    pub(crate) fn resolve_native<T>(&self, cx: &mut JSContext, val: &T)
189    where
190        T: ToJSValConvertible,
191    {
192        let mut realm = enter_auto_realm(cx, self);
193        let cx = &mut realm.current_realm();
194        rooted!(&in(cx) let mut v = UndefinedValue());
195        val.safe_to_jsval(cx, v.handle_mut());
196        self.resolve(cx, v.handle());
197    }
198
199    #[expect(unsafe_code)]
200    pub(crate) fn resolve(&self, cx: &mut JSContext, value: HandleValue) {
201        unsafe {
202            if !ResolvePromise(cx, self.promise_obj(), value) {
203                JS_ClearPendingException(cx);
204            }
205        }
206    }
207
208    pub(crate) fn reject_native<T>(&self, cx: &mut JSContext, val: &T)
209    where
210        T: ToJSValConvertible,
211    {
212        let mut realm = enter_auto_realm(cx, self);
213        let cx = &mut realm.current_realm();
214        rooted!(&in(cx) let mut v = UndefinedValue());
215        val.safe_to_jsval(cx, v.handle_mut());
216        self.reject(cx, v.handle());
217    }
218
219    pub(crate) fn reject_error(&self, cx: &mut JSContext, error: Error) {
220        let mut realm = enter_auto_realm(cx, self);
221        let cx = &mut realm.current_realm();
222        rooted!(&in(cx) let mut v = UndefinedValue());
223        error.to_jsval(cx, &self.global(), v.handle_mut());
224        self.reject(cx, v.handle());
225    }
226
227    #[expect(unsafe_code)]
228    pub(crate) fn reject(&self, cx: &mut JSContext, value: HandleValue) {
229        unsafe {
230            if !RejectPromise(cx, self.promise_obj(), value) {
231                JS_ClearPendingException(cx);
232            }
233        }
234    }
235
236    #[expect(unsafe_code)]
237    pub(crate) fn is_fulfilled(&self) -> bool {
238        let state = unsafe { GetPromiseState(self.promise_obj()) };
239        matches!(state, PromiseState::Rejected | PromiseState::Fulfilled)
240    }
241
242    #[expect(unsafe_code)]
243    pub(crate) fn is_rejected(&self) -> bool {
244        let state = unsafe { GetPromiseState(self.promise_obj()) };
245        matches!(state, PromiseState::Rejected)
246    }
247
248    #[expect(unsafe_code)]
249    pub(crate) fn is_pending(&self) -> bool {
250        let state = unsafe { GetPromiseState(self.promise_obj()) };
251        matches!(state, PromiseState::Pending)
252    }
253
254    #[expect(unsafe_code)]
255    pub(crate) fn promise_obj(&self) -> HandleObject<'_> {
256        let obj = self.reflector().get_jsobject();
257        unsafe {
258            assert!(IsPromiseObject(obj));
259        }
260        obj
261    }
262
263    #[expect(unsafe_code)]
264    pub(crate) fn append_native_handler(
265        &self,
266        cx: &mut CurrentRealm,
267        handler: &PromiseNativeHandler,
268    ) {
269        let global = GlobalScope::from_current_realm(cx);
270        run_a_script::<DomTypeHolder, _, _>(cx, &global, |cx| {
271            rooted!(&in(cx) let resolve_func =
272                create_native_handler_function(cx,
273                                               handler.reflector().get_jsobject(),
274                                               NativeHandlerTask::Resolve));
275
276            rooted!(&in(cx) let reject_func =
277                create_native_handler_function(cx,
278                                               handler.reflector().get_jsobject(),
279                                               NativeHandlerTask::Reject));
280
281            unsafe {
282                let ok = AddPromiseReactions(
283                    cx,
284                    self.promise_obj(),
285                    resolve_func.handle(),
286                    reject_func.handle(),
287                );
288                assert!(ok);
289            }
290        })
291    }
292
293    #[expect(unsafe_code)]
294    pub(crate) fn get_promise_is_handled(&self) -> bool {
295        unsafe { GetPromiseIsHandled(self.reflector().get_jsobject()) }
296    }
297
298    #[expect(unsafe_code)]
299    pub(crate) fn set_promise_is_handled(&self, cx: &mut JSContext) -> bool {
300        unsafe { SetAnyPromiseIsHandled(cx, self.reflector().get_jsobject()) }
301    }
302}
303
304#[expect(unsafe_code)]
305unsafe extern "C" fn do_nothing_promise_executor(
306    _cx: *mut RawJSContext,
307    argc: u32,
308    vp: *mut JSVal,
309) -> bool {
310    let args = unsafe { CallArgs::from_vp(vp, argc) };
311    args.rval().set(UndefinedValue());
312    true
313}
314
315const SLOT_NATIVEHANDLER: usize = 0;
316const SLOT_NATIVEHANDLER_TASK: usize = 1;
317
318#[derive(PartialEq)]
319enum NativeHandlerTask {
320    Resolve = 0,
321    Reject = 1,
322}
323
324#[expect(unsafe_code)]
325unsafe extern "C" fn native_handler_callback(
326    cx: *mut RawJSContext,
327    argc: u32,
328    vp: *mut JSVal,
329) -> bool {
330    // SAFETY: it is safe to construct a JSContext from engine hook.
331    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
332    let mut cx = CurrentRealm::assert(&mut cx);
333    let cx = &mut cx;
334
335    let args = unsafe { CallArgs::from_vp(vp, argc) };
336    let native_handler_value =
337        unsafe { *GetFunctionNativeReserved(args.callee(), SLOT_NATIVEHANDLER) };
338    rooted!(&in(cx) let native_handler_value = native_handler_value);
339    assert!(native_handler_value.get().is_object());
340
341    let handler =
342        unsafe { root_from_object::<PromiseNativeHandler>(cx, native_handler_value.to_object()) }
343            .expect("unexpected value for native handler in promise native handler callback");
344
345    let native_handler_task_value =
346        unsafe { *GetFunctionNativeReserved(args.callee(), SLOT_NATIVEHANDLER_TASK) };
347    rooted!(&in(cx) let native_handler_task_value = native_handler_task_value);
348    match native_handler_task_value.to_int32() {
349        native_handler_task_value
350            if native_handler_task_value == NativeHandlerTask::Resolve as i32 =>
351        {
352            handler.resolved_callback(cx, unsafe { HandleValue::from_raw(args.get(0)) })
353        },
354        native_handler_task_value
355            if native_handler_task_value == NativeHandlerTask::Reject as i32 =>
356        {
357            handler.rejected_callback(cx, unsafe { HandleValue::from_raw(args.get(0)) })
358        },
359        _ => panic!("unexpected native handler task value"),
360    };
361
362    true
363}
364
365#[expect(unsafe_code)]
366fn create_native_handler_function(
367    cx: &mut JSContext,
368    holder: HandleObject,
369    task: NativeHandlerTask,
370) -> *mut JSObject {
371    unsafe {
372        let func = NewFunctionWithReserved(cx, Some(native_handler_callback), 1, 0, ptr::null());
373        assert!(!func.is_null());
374
375        rooted!(&in(cx) let obj = JS_GetFunctionObject(func));
376        assert!(!obj.is_null());
377        SetFunctionNativeReserved(obj.get(), SLOT_NATIVEHANDLER, &ObjectValue(*holder));
378        SetFunctionNativeReserved(obj.get(), SLOT_NATIVEHANDLER_TASK, &Int32Value(task as i32));
379        obj.get()
380    }
381}
382
383impl FromJSValConvertibleRc for Promise {
384    fn safe_from_jsval(
385        cx: &mut JSContext,
386        value: HandleValue,
387    ) -> Result<ConversionResult<Rc<Promise>>, ()> {
388        if value.get().is_null() {
389            return Ok(ConversionResult::Failure(c"null not allowed".into()));
390        }
391
392        let mut realm = CurrentRealm::assert(cx);
393        let global_scope = GlobalScope::from_current_realm(&mut realm);
394
395        let promise = Promise::new_resolved(cx, &global_scope, value);
396        Ok(ConversionResult::Success(promise))
397    }
398}
399
400/// The success steps of <https://webidl.spec.whatwg.org/#wait-for-all>
401type WaitForAllSuccessSteps = Rc<dyn Fn(&mut JSContext, Vec<HandleValue>)>;
402
403/// The failure steps of <https://webidl.spec.whatwg.org/#wait-for-all>
404type WaitForAllFailureSteps = Rc<dyn Fn(&mut JSContext, HandleValue)>;
405
406/// The fulfillment handler for the list of promises in
407/// <https://webidl.spec.whatwg.org/#wait-for-all>.
408#[derive(JSTraceable, MallocSizeOf)]
409#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
410struct WaitForAllFulfillmentHandler {
411    /// The steps to call when all promises are resolved.
412    #[ignore_malloc_size_of = "callbacks are hard"]
413    #[no_trace]
414    success_steps: WaitForAllSuccessSteps,
415
416    /// The results of the promises.
417    #[ignore_malloc_size_of = "mozjs"]
418    #[expect(clippy::vec_box)]
419    result: Rc<RefCell<Vec<Box<Heap<JSVal>>>>>,
420
421    /// The index identifying which promise this handler is attached to.
422    promise_index: usize,
423
424    /// A count of fulfilled promises.
425    #[conditional_malloc_size_of]
426    fulfilled_count: Rc<Cell<usize>>,
427}
428
429impl Callback for WaitForAllFulfillmentHandler {
430    fn callback(&self, cx: &mut CurrentRealm, v: HandleValue) {
431        // Let fulfillmentHandler be the following steps given arg:
432
433        let equals_total = {
434            // Set result[promiseIndex] to arg.
435            let result = self.result.borrow_mut();
436            result[self.promise_index].set(v.get());
437
438            // Set fulfilledCount to fulfilledCount + 1.
439            let mut fulfilled_count = self.fulfilled_count.get();
440            fulfilled_count += 1;
441            self.fulfilled_count.set(fulfilled_count);
442
443            fulfilled_count == result.len()
444        };
445
446        // If fulfilledCount equals total, then perform successSteps given result.
447        if equals_total {
448            let result_ref = self.result.borrow();
449            let result_handles: Vec<HandleValue> =
450                result_ref.iter().map(|v| v.as_handle_value()).collect();
451
452            (self.success_steps)(cx, result_handles);
453        }
454    }
455}
456
457/// The rejection handler for the list of promises in
458/// <https://webidl.spec.whatwg.org/#wait-for-all>.
459#[derive(Clone, JSTraceable, MallocSizeOf)]
460struct WaitForAllRejectionHandler {
461    /// The steps to call if any promise rejects.
462    #[ignore_malloc_size_of = "callbacks are hard"]
463    #[no_trace]
464    failure_steps: WaitForAllFailureSteps,
465
466    /// Whether any promises have been rejected already.
467    rejected: Cell<bool>,
468}
469
470impl Callback for WaitForAllRejectionHandler {
471    fn callback(&self, cx: &mut CurrentRealm, v: HandleValue) {
472        // Let rejectionHandlerSteps be the following steps given arg:
473
474        if self.rejected.replace(true) {
475            // If rejected is true, abort these steps.
476            return;
477        }
478
479        // Set rejected to true.
480        // Done above with `replace`.
481        (self.failure_steps)(cx, v);
482    }
483}
484
485/// The microtask for performing successSteps given « » in
486/// <https://webidl.spec.whatwg.org/#wait-for-all>.
487#[derive(JSTraceable, MallocSizeOf)]
488#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
489pub(crate) struct WaitForAllSuccessStepsMicrotask {
490    global: Dom<GlobalScope>,
491
492    #[ignore_malloc_size_of = "Closure is hard"]
493    #[no_trace]
494    success_steps: WaitForAllSuccessSteps,
495}
496
497impl MicrotaskRunnable for WaitForAllSuccessStepsMicrotask {
498    fn handler(&self, cx: &mut JSContext) {
499        let mut realm = enter_auto_realm(cx, &*self.global);
500        (self.success_steps)(&mut realm, vec![]);
501    }
502}
503
504/// <https://webidl.spec.whatwg.org/#wait-for-all>
505#[cfg_attr(crown, expect(crown::unrooted_must_root))]
506fn wait_for_all(
507    cx: &mut CurrentRealm,
508    global: &GlobalScope,
509    promises: Vec<Rc<Promise>>,
510    success_steps: WaitForAllSuccessSteps,
511    failure_steps: WaitForAllFailureSteps,
512) {
513    // Let fulfilledCount be 0.
514    let fulfilled_count: Rc<Cell<usize>> = Default::default();
515
516    // Let rejected be false.
517    // Note: done below when constructing a rejection handler.
518
519    // Let rejectionHandlerSteps be the following steps given arg:
520    // Note: implemented with the `WaitForAllRejectionHandler`.
521
522    // Let rejectionHandler be CreateBuiltinFunction(rejectionHandlerSteps, « »):
523    // Note: done as part of attaching the `WaitForAllRejectionHandler` as native rejection handler.
524    let rejection_handler = WaitForAllRejectionHandler {
525        failure_steps,
526        rejected: Default::default(),
527    };
528
529    // Let total be promises’s size.
530    // Note: done using the len of result.
531
532    // If total is 0, then:
533    if promises.is_empty() {
534        // Queue a microtask to perform successSteps given « ».
535        global.enqueue_microtask(
536            cx,
537            Box::new(WaitForAllSuccessStepsMicrotask {
538                global: Dom::from_ref(global),
539                success_steps,
540            }),
541        );
542
543        // Return.
544        return;
545    }
546
547    // Let index be 0.
548    // Note: done with `enumerate` below.
549
550    // Let result be a list containing total null values.
551    let result: Rc<RefCell<Vec<Box<Heap<JSVal>>>>> = Default::default();
552
553    // For each promise of promises:
554    for (promise_index, promise) in promises.into_iter().enumerate() {
555        let result = result.clone();
556
557        {
558            // Note: adding a null value for this promise result.
559            let mut result_list = result.borrow_mut();
560            rooted!(&in(cx) let null_value = NullValue());
561            result_list.push(Heap::boxed(null_value.get()));
562        }
563
564        // Let promiseIndex be index.
565        // Note: done with `enumerate` above.
566
567        // Let fulfillmentHandler be the following steps given arg:
568        // Note: implemented with the `WaitForAllFulFillmentHandler`.
569
570        // Let fulfillmentHandler be CreateBuiltinFunction(fulfillmentHandler, « »):
571        // Note: passed below to avoid the need to root it.
572
573        // Perform PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler).
574        let handler = PromiseNativeHandler::new(
575            cx,
576            global,
577            Some(Box::new(WaitForAllFulfillmentHandler {
578                success_steps: success_steps.clone(),
579                result,
580                promise_index,
581                fulfilled_count: fulfilled_count.clone(),
582            })),
583            Some(Box::new(rejection_handler.clone())),
584        );
585        promise.append_native_handler(cx, &handler);
586
587        // Set index to index + 1.
588        // Note: done above with `enumerate`.
589    }
590}
591
592/// <https://webidl.spec.whatwg.org/#waiting-for-all-promise>
593pub(crate) fn wait_for_all_promise(
594    cx: &mut CurrentRealm,
595    global: &GlobalScope,
596    promises: Vec<Rc<Promise>>,
597) -> Rc<Promise> {
598    // Let promise be a new promise of type Promise<sequence<T>> in realm.
599    let promise = Promise::new(cx, global);
600    let success_promise = promise.clone();
601    let failure_promise = promise.clone();
602
603    // Let successSteps be the following steps, given results:
604    let success_steps = Rc::new(move |cx: &mut JSContext, results: Vec<HandleValue>| {
605        // Resolve promise with results.
606        success_promise.resolve_native(cx, &results);
607    });
608
609    // Let failureSteps be the following steps, given reason:
610    let failure_steps = Rc::new(move |cx: &mut JSContext, reason: HandleValue| {
611        // Reject promise with reason.
612        failure_promise.reject_native(cx, &reason);
613    });
614
615    // Wait for all with promises, given successSteps and failureSteps.
616    wait_for_all(cx, global, promises, success_steps, failure_steps);
617
618    // Return promise.
619    promise
620}