ul_next/javascript/
object.rs

1use core::fmt;
2use std::{
3    ops::Deref,
4    sync::{Arc, OnceLock},
5};
6
7use crate::Library;
8
9use super::{AsJSValue, JSContext, JSString, JSValue};
10
11// TODO: major hack, not sure how to get access to the Library
12//       from inside the trampoline
13static LIBRARY: OnceLock<Arc<Library>> = OnceLock::new();
14
15/// Attributes for JavaScript properties.
16///
17/// Used in [`JSObject::set_property`] and [`JSObject::set_property_for_key`].
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub struct JSPropertyAttributes {
20    /// Specifies that a property is read-only.
21    pub read_only: bool,
22    /// Specifies that a property should not be enumerated by `JSPropertyEnumerators`
23    /// and JavaScript `for...in` loops.
24    pub dont_enum: bool,
25    /// Specifies that the delete operation should fail on a property.
26    pub dont_delete: bool,
27}
28
29impl JSPropertyAttributes {
30    /// Creates empty attributes.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Specifies that a property is read-only.
36    pub fn read_only(mut self, read_only: bool) -> Self {
37        self.read_only = read_only;
38        self
39    }
40
41    /// Specifies that a property should not be enumerated by `JSPropertyEnumerators`
42    /// and JavaScript `for...in` loops.
43    pub fn dont_enum(mut self, dont_enum: bool) -> Self {
44        self.dont_enum = dont_enum;
45        self
46    }
47
48    /// Specifies that the delete operation should fail on a property.
49    pub fn dont_delete(mut self, dont_delete: bool) -> Self {
50        self.dont_delete = dont_delete;
51        self
52    }
53
54    pub(crate) fn to_raw(self) -> u32 {
55        let mut raw = 0;
56
57        if self.read_only {
58            raw |= ul_sys::kJSPropertyAttributeReadOnly;
59        }
60
61        if self.dont_enum {
62            raw |= ul_sys::kJSPropertyAttributeDontEnum;
63        }
64
65        if self.dont_delete {
66            raw |= ul_sys::kJSPropertyAttributeDontDelete;
67        }
68
69        raw
70    }
71}
72
73/// A JavaScript object.
74#[derive(Clone, Debug)]
75pub struct JSObject<'a> {
76    pub(crate) value: JSValue<'a>,
77}
78
79impl<'a> JSObject<'a> {
80    pub(crate) fn copy_from_raw(ctx: &'a JSContext, obj: ul_sys::JSObjectRef) -> Self {
81        assert!(!obj.is_null());
82
83        // add one
84        unsafe { ctx.lib.ultralight().JSValueProtect(ctx.internal, obj) };
85
86        Self {
87            value: JSValue::from_raw(ctx, obj),
88        }
89    }
90
91    /// Create a new default javascript object.
92    pub fn new(ctx: &'a JSContext) -> Self {
93        let obj = unsafe {
94            ctx.lib.ultralight().JSObjectMake(
95                ctx.internal,
96                std::ptr::null_mut(),
97                std::ptr::null_mut(),
98            )
99        };
100
101        Self {
102            value: JSValue::from_raw(ctx, obj),
103        }
104    }
105
106    /// Create a JavaScript function with a given callback as its implementation.
107    ///
108    /// Results in a JSObject that is a function. The object's prototype will be the default function prototype.
109    ///
110    /// This can be used to execute Rust code from JavaScript.
111    pub fn new_function_with_callback<F>(ctx: &'a JSContext, callback: F) -> Self
112    where
113        for<'c> F:
114            FnMut(&'c JSContext, &JSObject<'c>, &[JSValue<'c>]) -> Result<JSValue<'c>, JSValue<'c>>,
115    {
116        LIBRARY.get_or_init(|| ctx.lib.clone());
117
118        unsafe extern "C" fn finalize<Env>(function: ul_sys::JSObjectRef)
119        where
120            for<'c> Env: FnMut(
121                &'c JSContext,
122                &JSObject<'c>,
123                &[JSValue<'c>],
124            ) -> Result<JSValue<'c>, JSValue<'c>>,
125        {
126            let _guard = scopeguard::guard_on_unwind((), |()| {
127                ::std::process::abort();
128            });
129
130            let lib = ffi_unwrap!(LIBRARY.get(), "library undefined ptr",);
131
132            let private_data = lib.ultralight().JSObjectGetPrivate(function) as *mut Box<Env>;
133
134            let _ = Box::from_raw(private_data);
135        }
136
137        unsafe extern "C" fn trampoline<Env>(
138            ctx: ul_sys::JSContextRef,
139            function: ul_sys::JSObjectRef,
140            this_object: ul_sys::JSObjectRef,
141            argument_count: usize,
142            arguments: *const ul_sys::JSValueRef,
143            exception: *mut ul_sys::JSValueRef,
144        ) -> ul_sys::JSValueRef
145        where
146            for<'c> Env: FnMut(
147                &'c JSContext,
148                &JSObject<'c>,
149                &[JSValue<'c>],
150            ) -> Result<JSValue<'c>, JSValue<'c>>,
151        {
152            let _guard = scopeguard::guard_on_unwind((), |()| {
153                ::std::process::abort();
154            });
155
156            let lib = ffi_unwrap!(LIBRARY.get(), "library undefined ptr",);
157
158            let private_data = lib.ultralight().JSObjectGetPrivate(function) as *mut Box<Env>;
159            let callback: &mut Box<Env> = ffi_unwrap!(private_data.as_mut(), "null ptr",);
160
161            let ctx = JSContext::copy_from_raw(lib.clone(), ctx);
162            let this = JSObject::copy_from_raw(&ctx, this_object);
163            let args = std::slice::from_raw_parts(arguments, argument_count)
164                .iter()
165                .map(|v| JSValue::copy_from_raw(&ctx, *v))
166                .collect::<Vec<_>>();
167
168            let ret = callback(&ctx, &this, &args);
169            match ret {
170                Ok(value) => value.into_raw(),
171                Err(value) => {
172                    if !exception.is_null() {
173                        *exception = value.into_raw();
174                    }
175                    std::ptr::null_mut()
176                }
177            }
178        }
179
180        let c_callback: ul_sys::JSObjectCallAsFunctionCallback = Some(trampoline::<F>);
181        let callback_data: *mut Box<F> = Box::into_raw(Box::new(Box::new(callback)));
182
183        let class_def = ul_sys::JSClassDefinition {
184            finalize: Some(finalize::<F>),
185            callAsFunction: c_callback,
186            ..super::class::EMPTY_CLASS_DEF
187        };
188
189        let obj = unsafe {
190            let class = ctx.lib.ultralight().JSClassCreate(&class_def);
191
192            let obj = ctx
193                .lib
194                .ultralight()
195                .JSObjectMake(ctx.internal, class, callback_data as _);
196
197            ctx.lib.ultralight().JSClassRelease(class);
198
199            obj
200        };
201
202        Self {
203            value: JSValue::from_raw(ctx, obj),
204        }
205    }
206
207    /// Creates a function with a given script as its body.
208    ///
209    /// Use this method when you want to execute a script repeatedly,
210    /// to avoid the cost of re-parsing the script before each execution.
211    ///
212    /// Can return [`Err`] if there was an exception when creating the function or
213    /// if the script or parameters contain syntax errors.
214    pub fn new_function(
215        ctx: &'a JSContext,
216        name: Option<&str>,
217        param_names: &[&str],
218        body: &str,
219        source_url: Option<&str>,
220        starting_line_number: i32,
221    ) -> Result<Self, JSValue<'a>> {
222        let mut exception = std::ptr::null();
223        let name = name.map(|n| JSString::new(ctx.lib.clone(), n));
224
225        let param_names: Vec<_> = param_names
226            .iter()
227            .map(|n| JSString::new(ctx.lib.clone(), n))
228            .collect();
229        let params_ptrs: Vec<_> = param_names.iter().map(|n| n.internal).collect();
230
231        let body = JSString::new(ctx.lib.clone(), body);
232        let source_url = source_url.map(|s| JSString::new(ctx.lib.clone(), s));
233
234        let obj = unsafe {
235            ctx.lib.ultralight().JSObjectMakeFunction(
236                ctx.internal,
237                name.map_or(std::ptr::null_mut(), |n| n.internal),
238                param_names.len() as _,
239                if param_names.is_empty() {
240                    std::ptr::null()
241                } else {
242                    params_ptrs.as_ptr()
243                },
244                body.internal,
245                source_url.map_or(std::ptr::null_mut(), |s| s.internal),
246                starting_line_number,
247                &mut exception,
248            )
249        };
250
251        if !exception.is_null() {
252            Err(JSValue::from_raw(ctx, exception))
253        } else if obj.is_null() {
254            Err(JSValue::new_string(ctx, "Failed to create function"))
255        } else {
256            Ok(Self {
257                value: JSValue::from_raw(ctx, obj),
258            })
259        }
260    }
261
262    /// Creates a JavaScript Array object.
263    pub fn new_array(ctx: &'a JSContext, items: &[JSValue]) -> Result<Self, JSValue<'a>> {
264        let items_ptrs: Vec<_> = items.iter().map(|v| v.internal).collect();
265
266        let mut exception = std::ptr::null();
267
268        let result = unsafe {
269            ctx.lib.ultralight().JSObjectMakeArray(
270                ctx.internal,
271                items.len() as _,
272                items_ptrs.as_ptr(),
273                &mut exception,
274            )
275        };
276
277        if !exception.is_null() {
278            Err(JSValue::from_raw(ctx, exception))
279        } else if result.is_null() {
280            Err(JSValue::new_string(ctx, "Failed to create array"))
281        } else {
282            Ok(Self {
283                value: JSValue::from_raw(ctx, result),
284            })
285        }
286    }
287
288    /// Tests whether an object can be called as a function.
289    pub fn is_function(&self) -> bool {
290        unsafe {
291            self.value
292                .ctx
293                .lib
294                .ultralight()
295                .JSObjectIsFunction(self.value.ctx.internal, self.value.internal as _)
296        }
297    }
298
299    /// Tests whether an object can be called as a constructor.
300    pub fn is_constructor(&self) -> bool {
301        unsafe {
302            self.value
303                .ctx
304                .lib
305                .ultralight()
306                .JSObjectIsConstructor(self.value.ctx.internal, self.value.internal as _)
307        }
308    }
309
310    /// Calls an object as a function.
311    ///
312    /// Return the [`JSValue`] that results from calling object as a function,
313    /// or [`Err`] if an exception is thrown or object is not a function.
314    pub fn call_as_function(
315        &self,
316        this: Option<&JSObject>,
317        args: &[JSValue],
318    ) -> Result<JSValue, JSValue> {
319        let mut exception = std::ptr::null();
320
321        let args: Vec<_> = args.iter().map(|v| v.internal).collect();
322
323        let result_raw = unsafe {
324            self.value.ctx.lib.ultralight().JSObjectCallAsFunction(
325                self.value.ctx.internal,
326                self.value.internal as _,
327                this.map_or(std::ptr::null_mut(), |v| v.internal as _),
328                args.len(),
329                args.as_ptr(),
330                &mut exception,
331            )
332        };
333
334        if !exception.is_null() {
335            Err(JSValue::from_raw(self.value.ctx, exception))
336        } else if result_raw.is_null() {
337            Err(JSValue::new_string(
338                self.value.ctx,
339                "Failed to call function",
340            ))
341        } else {
342            Ok(JSValue::from_raw(self.value.ctx, result_raw))
343        }
344    }
345
346    /// Calls an object as a constructor.
347    ///
348    /// Return the [`JSObject`] that results from calling object as a constructor,
349    /// or [`Err`] if an exception is thrown or object is not a constructor.
350    pub fn call_as_constructor(&self, args: &[JSValue]) -> Result<JSObject, JSValue> {
351        let mut exception = std::ptr::null();
352
353        let args: Vec<_> = args.iter().map(|v| v.internal).collect();
354
355        let result_raw = unsafe {
356            self.value.ctx.lib.ultralight().JSObjectCallAsConstructor(
357                self.value.ctx.internal,
358                self.value.internal as _,
359                args.len(),
360                args.as_ptr(),
361                &mut exception,
362            )
363        };
364
365        if !exception.is_null() {
366            Err(JSValue::from_raw(self.value.ctx, exception))
367        } else if result_raw.is_null() {
368            Err(JSValue::new_string(
369                self.value.ctx,
370                "Failed to call constructor",
371            ))
372        } else {
373            Ok(JSObject::copy_from_raw(self.value.ctx, result_raw))
374        }
375    }
376}
377
378impl JSObject<'_> {
379    /// Gets a property from an object by name.
380    ///
381    /// Returns the property's value if object has the property, otherwise the undefined value,
382    /// or [`Err`] if an exception is thrown.
383    pub fn get_property(&self, name: &str) -> Result<JSValue, JSValue> {
384        let name = JSString::new(self.ctx.lib.clone(), name);
385        let mut exception = std::ptr::null();
386
387        let result_raw = unsafe {
388            self.ctx.lib.ultralight().JSObjectGetProperty(
389                self.ctx.internal,
390                self.internal as _,
391                name.internal,
392                &mut exception,
393            )
394        };
395
396        if !exception.is_null() {
397            Err(JSValue::from_raw(self.ctx, exception))
398        } else if result_raw.is_null() {
399            Err(JSValue::new_string(self.ctx, "Failed to get property"))
400        } else {
401            Ok(JSValue::from_raw(self.ctx, result_raw))
402        }
403    }
404
405    /// Gets a property from an object by numeric index.
406    ///
407    /// Returns the property's value if object has the property, otherwise the undefined value,
408    /// or [`Err`] if an exception is thrown.
409    ///
410    /// Calling [`JSObject::get_property_at_index`] is equivalent to calling [`JSObject::get_property`]
411    /// with a string containing `index`, but [`JSObject::get_property_at_index`] provides optimized
412    /// access to numeric properties.
413    pub fn get_property_at_index(&self, index: u32) -> Result<JSValue, JSValue> {
414        let mut exception = std::ptr::null();
415
416        let result_raw = unsafe {
417            self.ctx.lib.ultralight().JSObjectGetPropertyAtIndex(
418                self.ctx.internal,
419                self.internal as _,
420                index,
421                &mut exception,
422            )
423        };
424
425        if !exception.is_null() {
426            Err(JSValue::from_raw(self.ctx, exception))
427        } else if result_raw.is_null() {
428            Err(JSValue::new_string(self.ctx, "Failed to get property"))
429        } else {
430            Ok(JSValue::from_raw(self.ctx, result_raw))
431        }
432    }
433
434    /// Sets a property on an object by name.
435    ///
436    /// Returns [`Err`] if an exception is thrown.
437    pub fn set_property(
438        &self,
439        name: &str,
440        value: &JSValue,
441        attributes: JSPropertyAttributes,
442    ) -> Result<(), JSValue> {
443        let name = JSString::new(self.ctx.lib.clone(), name);
444        let mut exception = std::ptr::null();
445
446        unsafe {
447            self.ctx.lib.ultralight().JSObjectSetProperty(
448                self.ctx.internal,
449                self.internal as _,
450                name.internal,
451                value.internal,
452                attributes.to_raw(),
453                &mut exception,
454            );
455        }
456
457        if !exception.is_null() {
458            Err(JSValue::from_raw(self.ctx, exception))
459        } else {
460            Ok(())
461        }
462    }
463
464    /// Sets a property on an object by numeric index.
465    ///
466    /// Returns [`Err`] if an exception is thrown.
467    ///
468    /// Calling [`JSObject::set_property_at_index`] is equivalent to calling
469    /// [`JSObject::set_property`] with a string containing `index`,
470    /// but [`JSObject::set_property_at_index`] provides optimized access to numeric properties.
471    pub fn set_property_at_index(&self, index: u32, value: &JSValue) -> Result<(), JSValue> {
472        let mut exception = std::ptr::null();
473
474        unsafe {
475            self.ctx.lib.ultralight().JSObjectSetPropertyAtIndex(
476                self.ctx.internal,
477                self.internal as _,
478                index,
479                value.internal,
480                &mut exception,
481            );
482        }
483
484        if !exception.is_null() {
485            Err(JSValue::from_raw(self.ctx, exception))
486        } else {
487            Ok(())
488        }
489    }
490
491    /// Gets the names of an object's enumerable properties.
492    pub fn get_property_names(&self) -> JSPropertyNameArray {
493        let names = unsafe {
494            self.ctx
495                .lib
496                .ultralight()
497                .JSObjectCopyPropertyNames(self.ctx.internal, self.internal as _)
498        };
499
500        JSPropertyNameArray::from_raw(self.ctx, names)
501    }
502
503    /// Tests whether an object has a property.
504    pub fn has_property(&self, name: &str) -> bool {
505        let name = JSString::new(self.ctx.lib.clone(), name);
506
507        unsafe {
508            self.ctx.lib.ultralight().JSObjectHasProperty(
509                self.ctx.internal,
510                self.internal as _,
511                name.internal,
512            )
513        }
514    }
515
516    /// Deletes a property from an object by name.
517    ///
518    /// Returns `true` if the property was deleted, `false` if the property was not present,
519    /// or it had [`JSPropertyAttributes::dont_delete`] set, or [`Err`] if an exception is thrown.
520    pub fn delete_property(&self, name: &str) -> Result<bool, JSValue> {
521        let name = JSString::new(self.ctx.lib.clone(), name);
522        let mut exception = std::ptr::null();
523
524        let result = unsafe {
525            self.ctx.lib.ultralight().JSObjectDeleteProperty(
526                self.ctx.internal,
527                self.internal as _,
528                name.internal,
529                &mut exception,
530            )
531        };
532
533        if !exception.is_null() {
534            Err(JSValue::from_raw(self.ctx, exception))
535        } else {
536            Ok(result)
537        }
538    }
539
540    /// Gets a property from an object using a [`JSValue`] as the property key.
541    ///
542    /// Returns the property's value if object has the property, otherwise the undefined value,
543    /// or [`Err`] if an exception is thrown.
544    ///
545    /// This function is the same as performing `object[propertyKey](propertyKey)` from JavaScript.
546    pub fn get_property_for_key(&self, key: &JSValue) -> Result<JSValue, JSValue> {
547        let mut exception = std::ptr::null();
548
549        let result_raw = unsafe {
550            self.ctx.lib.ultralight().JSObjectGetPropertyForKey(
551                self.ctx.internal,
552                self.internal as _,
553                key.internal,
554                &mut exception,
555            )
556        };
557
558        if !exception.is_null() {
559            Err(JSValue::from_raw(self.ctx, exception))
560        } else if result_raw.is_null() {
561            Err(JSValue::new_string(self.ctx, "Failed to get property"))
562        } else {
563            Ok(JSValue::from_raw(self.ctx, result_raw))
564        }
565    }
566
567    /// Sets a property on an object using a [`JSValue`] as the property key.
568    ///
569    /// Returns [`Err`] if an exception is thrown.
570    ///
571    /// This function is the same as performing `object[propertyKey](propertyKey) = value` from JavaScript.
572    pub fn set_property_for_key(
573        &self,
574        key: &JSValue,
575        value: &JSValue,
576        attributes: JSPropertyAttributes,
577    ) -> Result<(), JSValue> {
578        let mut exception = std::ptr::null();
579
580        unsafe {
581            self.ctx.lib.ultralight().JSObjectSetPropertyForKey(
582                self.ctx.internal,
583                self.internal as _,
584                key.internal,
585                value.internal,
586                attributes.to_raw(),
587                &mut exception,
588            );
589        }
590
591        if !exception.is_null() {
592            Err(JSValue::from_raw(self.ctx, exception))
593        } else {
594            Ok(())
595        }
596    }
597
598    /// Tests whether an object has a given property using a [`JSValue`] as the property key.
599    ///
600    /// This function is the same as performing `propertyKey in object` from JavaScript.
601    pub fn has_property_for_key(&self, key: &JSValue) -> Result<bool, JSValue> {
602        let mut exception = std::ptr::null();
603
604        let result = unsafe {
605            self.ctx.lib.ultralight().JSObjectHasPropertyForKey(
606                self.ctx.internal,
607                self.internal as _,
608                key.internal,
609                &mut exception,
610            )
611        };
612
613        if !exception.is_null() {
614            Err(JSValue::from_raw(self.ctx, exception))
615        } else {
616            Ok(result)
617        }
618    }
619
620    /// Deletes a property from an object using a [`JSValue`] as the property key.
621    ///
622    /// Returns `true` if the property was deleted, `false` if the property was not present,
623    /// or it had [`JSPropertyAttributes::dont_delete`] set, or [`Err`] if an exception is thrown.
624    ///
625    /// This function is the same as performing `delete object[propertyKey](propertyKey)` from JavaScript.
626    pub fn delete_property_for_key(&self, key: &JSValue) -> Result<bool, JSValue> {
627        let mut exception = std::ptr::null();
628
629        let result = unsafe {
630            self.ctx.lib.ultralight().JSObjectDeletePropertyForKey(
631                self.ctx.internal,
632                self.internal as _,
633                key.internal,
634                &mut exception,
635            )
636        };
637
638        if !exception.is_null() {
639            Err(JSValue::from_raw(self.ctx, exception))
640        } else {
641            Ok(result)
642        }
643    }
644}
645
646impl<'a> AsRef<JSValue<'a>> for JSObject<'a> {
647    fn as_ref(&self) -> &JSValue<'a> {
648        &self.value
649    }
650}
651
652impl<'a> Deref for JSObject<'a> {
653    type Target = JSValue<'a>;
654
655    fn deref(&self) -> &Self::Target {
656        &self.value
657    }
658}
659
660impl<'a> AsJSValue<'a> for JSObject<'a> {
661    fn into_value(self) -> JSValue<'a> {
662        self.value
663    }
664
665    fn as_value(&self) -> &JSValue<'a> {
666        &self.value
667    }
668}
669
670/// A reference to an array of property names.
671///
672/// This is created by [`JSObject::get_property_names`].
673pub struct JSPropertyNameArray<'a> {
674    internal: ul_sys::JSPropertyNameArrayRef,
675    ctx: &'a JSContext,
676}
677
678impl<'a> JSPropertyNameArray<'a> {
679    pub(crate) fn from_raw(ctx: &'a JSContext, array: ul_sys::JSPropertyNameArrayRef) -> Self {
680        assert!(!array.is_null());
681
682        Self {
683            internal: array,
684            ctx,
685        }
686    }
687
688    /// Returns `true` if the array is empty.
689    pub fn is_empty(&self) -> bool {
690        self.len() == 0
691    }
692
693    /// Returns the number of property names in the array.
694    pub fn len(&self) -> usize {
695        unsafe {
696            self.ctx
697                .lib
698                .ultralight()
699                .JSPropertyNameArrayGetCount(self.internal)
700        }
701    }
702
703    /// Gets a property name at a given index.
704    pub fn get(&self, index: usize) -> Option<JSString> {
705        let name = unsafe {
706            self.ctx
707                .lib
708                .ultralight()
709                .JSPropertyNameArrayGetNameAtIndex(self.internal, index)
710        };
711
712        if name.is_null() {
713            None
714        } else {
715            Some(JSString::copy_from_raw(self.ctx.lib.clone(), name))
716        }
717    }
718
719    /// Converts the array into a [`Vec`] of property names.
720    pub fn into_vec(self) -> Vec<String> {
721        self.into()
722    }
723}
724
725impl From<JSPropertyNameArray<'_>> for Vec<String> {
726    fn from(array: JSPropertyNameArray) -> Self {
727        let mut names = Vec::with_capacity(array.len());
728
729        for i in 0..array.len() {
730            let name = array
731                .get(i)
732                .expect("Array should still have elements")
733                .to_string();
734            names.push(name);
735        }
736
737        names
738    }
739}
740
741impl Clone for JSPropertyNameArray<'_> {
742    fn clone(&self) -> Self {
743        let array = unsafe {
744            self.ctx
745                .lib
746                .ultralight()
747                .JSPropertyNameArrayRetain(self.internal)
748        };
749
750        Self {
751            internal: array,
752            ctx: self.ctx,
753        }
754    }
755}
756
757impl fmt::Debug for JSPropertyNameArray<'_> {
758    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759        self.clone().into_vec().fmt(f)
760    }
761}
762
763impl Drop for JSPropertyNameArray<'_> {
764    fn drop(&mut self) {
765        unsafe {
766            self.ctx
767                .lib
768                .ultralight()
769                .JSPropertyNameArrayRelease(self.internal);
770        }
771    }
772}