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