script_bindings/
callback.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//! Base classes to work with IDL callbacks.
6
7use std::default::Default;
8use std::ffi::CString;
9use std::rc::Rc;
10
11use js::jsapi::{
12    AddRawValueRoot, EnterRealm, Heap, IsCallable, JSObject, LeaveRealm, RemoveRawValueRoot,
13};
14use js::jsval::{JSVal, NullValue, ObjectValue, UndefinedValue};
15use js::rust::wrappers::{JS_GetProperty, JS_WrapObject};
16use js::rust::{HandleObject, MutableHandleValue, Runtime};
17
18use crate::codegen::GenericBindings::WindowBinding::Window_Binding::WindowMethods;
19use crate::error::{Error, Fallible};
20use crate::inheritance::Castable;
21use crate::interfaces::{DocumentHelpers, DomHelpers, GlobalScopeHelpers};
22use crate::realms::{InRealm, enter_realm};
23use crate::reflector::DomObject;
24use crate::root::Dom;
25use crate::script_runtime::{CanGc, JSContext};
26use crate::settings_stack::{run_a_callback, run_a_script};
27use crate::{DomTypes, cformat};
28
29pub trait ThisReflector {
30    fn jsobject(&self) -> *mut JSObject;
31}
32
33impl<T: DomObject> ThisReflector for T {
34    fn jsobject(&self) -> *mut JSObject {
35        self.reflector().get_jsobject().get()
36    }
37}
38
39impl ThisReflector for HandleObject<'_> {
40    fn jsobject(&self) -> *mut JSObject {
41        self.get()
42    }
43}
44
45/// The exception handling used for a call.
46#[derive(Clone, Copy, PartialEq)]
47pub enum ExceptionHandling {
48    /// Report any exception and don't throw it to the caller code.
49    Report,
50    /// Throw any exception to the caller code.
51    Rethrow,
52}
53
54/// A common base class for representing IDL callback function and
55/// callback interface types.
56#[derive(JSTraceable, MallocSizeOf)]
57#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
58pub struct CallbackObject<D: DomTypes> {
59    /// The underlying `JSObject`.
60    #[ignore_malloc_size_of = "measured by mozjs"]
61    callback: Heap<*mut JSObject>,
62    #[ignore_malloc_size_of = "measured by mozjs"]
63    permanent_js_root: Heap<JSVal>,
64
65    /// The ["callback context"], that is, the global to use as incumbent
66    /// global when calling the callback.
67    ///
68    /// Looking at the WebIDL standard, it appears as though there would always
69    /// be a value here, but [sometimes] callback functions are created by
70    /// hand-waving without defining the value of the callback context, and
71    /// without any JavaScript code on the stack to grab an incumbent global
72    /// from.
73    ///
74    /// ["callback context"]: https://heycam.github.io/webidl/#dfn-callback-context
75    /// [sometimes]: https://github.com/whatwg/html/issues/2248
76    incumbent: Option<Dom<D::GlobalScope>>,
77}
78
79impl<D: DomTypes> CallbackObject<D> {
80    // These are used by the bindings and do not need `default()` functions.
81    #[allow(clippy::new_without_default)]
82    fn new() -> Self {
83        Self {
84            callback: Heap::default(),
85            permanent_js_root: Heap::default(),
86            incumbent: D::GlobalScope::incumbent().map(|i| Dom::from_ref(&*i)),
87        }
88    }
89
90    pub fn get(&self) -> *mut JSObject {
91        self.callback.get()
92    }
93
94    #[expect(unsafe_code)]
95    unsafe fn init(&mut self, cx: JSContext, callback: *mut JSObject) {
96        self.callback.set(callback);
97        self.permanent_js_root.set(ObjectValue(callback));
98        unsafe {
99            assert!(AddRawValueRoot(
100                *cx,
101                self.permanent_js_root.get_unsafe(),
102                c"CallbackObject::root".as_ptr()
103            ));
104        }
105    }
106}
107
108impl<D: DomTypes> Drop for CallbackObject<D> {
109    #[expect(unsafe_code)]
110    fn drop(&mut self) {
111        unsafe {
112            if let Some(cx) = Runtime::get() {
113                RemoveRawValueRoot(cx.as_ptr(), self.permanent_js_root.get_unsafe());
114            }
115        }
116    }
117}
118
119impl<D: DomTypes> PartialEq for CallbackObject<D> {
120    fn eq(&self, other: &CallbackObject<D>) -> bool {
121        self.callback.get() == other.callback.get()
122    }
123}
124
125/// A trait to be implemented by concrete IDL callback function and
126/// callback interface types.
127pub trait CallbackContainer<D: DomTypes> {
128    /// Create a new CallbackContainer object for the given `JSObject`.
129    ///
130    /// # Safety
131    /// `callback` must point to a valid, non-null JSObject.
132    unsafe fn new(cx: JSContext, callback: *mut JSObject) -> Rc<Self>;
133    /// Returns the underlying `CallbackObject`.
134    fn callback_holder(&self) -> &CallbackObject<D>;
135    /// Returns the underlying `JSObject`.
136    fn callback(&self) -> *mut JSObject {
137        self.callback_holder().get()
138    }
139    /// Returns the ["callback context"], that is, the global to use as
140    /// incumbent global when calling the callback.
141    ///
142    /// ["callback context"]: https://heycam.github.io/webidl/#dfn-callback-context
143    fn incumbent(&self) -> Option<&D::GlobalScope> {
144        self.callback_holder().incumbent.as_deref()
145    }
146}
147
148/// A common base class for representing IDL callback function types.
149#[derive(JSTraceable, MallocSizeOf, PartialEq)]
150#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
151pub struct CallbackFunction<D: DomTypes> {
152    object: CallbackObject<D>,
153}
154
155impl<D: DomTypes> CallbackFunction<D> {
156    /// Create a new `CallbackFunction` for this object.
157    // These are used by the bindings and do not need `default()` functions.
158    #[expect(clippy::new_without_default)]
159    pub fn new() -> Self {
160        Self {
161            object: CallbackObject::new(),
162        }
163    }
164
165    /// Returns the underlying `CallbackObject`.
166    pub fn callback_holder(&self) -> &CallbackObject<D> {
167        &self.object
168    }
169
170    /// Initialize the callback function with a value.
171    /// Should be called once this object is done moving.
172    ///
173    /// # Safety
174    /// `callback` must point to a valid, non-null JSObject.
175    pub unsafe fn init(&mut self, cx: JSContext, callback: *mut JSObject) {
176        unsafe { self.object.init(cx, callback) };
177    }
178}
179
180/// A common base class for representing IDL callback interface types.
181#[derive(JSTraceable, MallocSizeOf, PartialEq)]
182#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
183pub struct CallbackInterface<D: DomTypes> {
184    object: CallbackObject<D>,
185}
186
187impl<D: DomTypes> CallbackInterface<D> {
188    /// Create a new CallbackInterface object for the given `JSObject`.
189    // These are used by the bindings and do not need `default()` functions.
190    #[expect(clippy::new_without_default)]
191    pub fn new() -> Self {
192        Self {
193            object: CallbackObject::new(),
194        }
195    }
196
197    /// Returns the underlying `CallbackObject`.
198    pub fn callback_holder(&self) -> &CallbackObject<D> {
199        &self.object
200    }
201
202    /// Initialize the callback function with a value.
203    /// Should be called once this object is done moving.
204    ///
205    /// # Safety
206    /// `callback` must point to a valid, non-null JSObject.
207    pub unsafe fn init(&mut self, cx: JSContext, callback: *mut JSObject) {
208        unsafe { self.object.init(cx, callback) };
209    }
210
211    /// Returns the property with the given `name`, if it is a callable object,
212    /// or an error otherwise.
213    pub fn get_callable_property(&self, cx: JSContext, name: &str) -> Fallible<JSVal> {
214        rooted!(in(*cx) let mut callable = UndefinedValue());
215        rooted!(in(*cx) let obj = self.callback_holder().get());
216        unsafe {
217            let c_name = CString::new(name).unwrap();
218            if !JS_GetProperty(*cx, obj.handle(), c_name.as_ptr(), callable.handle_mut()) {
219                return Err(Error::JSFailed);
220            }
221
222            if !callable.is_object() || !IsCallable(callable.to_object()) {
223                return Err(Error::Type(cformat!(
224                    "The value of the {} property is not callable",
225                    name
226                )));
227            }
228        }
229        Ok(callable.get())
230    }
231}
232
233/// Wraps the reflector for `p` into the realm of `cx`.
234pub(crate) fn wrap_call_this_value<T: ThisReflector>(
235    cx: JSContext,
236    p: &T,
237    mut rval: MutableHandleValue,
238) -> bool {
239    rooted!(in(*cx) let mut obj = p.jsobject());
240
241    if obj.is_null() {
242        rval.set(NullValue());
243        return true;
244    }
245
246    unsafe {
247        if !JS_WrapObject(*cx, obj.handle_mut()) {
248            return false;
249        }
250    }
251
252    rval.set(ObjectValue(*obj));
253    true
254}
255
256/// A function wrapper that performs whatever setup we need to safely make a call.
257///
258/// <https://webidl.spec.whatwg.org/#es-invoking-callback-functions>
259pub fn call_setup<D: DomTypes, T: CallbackContainer<D>, R>(
260    callback: &T,
261    handling: ExceptionHandling,
262    f: impl FnOnce(JSContext) -> R,
263) -> R {
264    // The global for reporting exceptions. This is the global object of the
265    // (possibly wrapped) callback object.
266    let global = unsafe { D::GlobalScope::from_object(callback.callback()) };
267    if let Some(window) = global.downcast::<D::Window>() {
268        window.Document().ensure_safe_to_run_script_or_layout();
269    }
270    let cx = D::GlobalScope::get_cx();
271
272    let global = &global;
273
274    // Step 8: Prepare to run script with relevant settings.
275    run_a_script::<D, R>(global, move || {
276        let actual_callback = || {
277            let old_realm = unsafe { EnterRealm(*cx, callback.callback()) };
278            let result = f(cx);
279            unsafe {
280                LeaveRealm(*cx, old_realm);
281            }
282            if handling == ExceptionHandling::Report {
283                let ar = enter_realm::<D>(&**global);
284                <D as DomHelpers<D>>::report_pending_exception(
285                    cx,
286                    InRealm::Entered(&ar),
287                    CanGc::note(),
288                );
289            }
290            result
291        };
292        if let Some(incumbent_global) = callback.incumbent() {
293            // Step 9: Prepare to run a callback with stored settings.
294            run_a_callback::<D, R>(incumbent_global, actual_callback)
295        } else {
296            actual_callback()
297        }
298    }) // Step 14.2: Clean up after running script with relevant settings.
299}