deft_quick_js/bindings/
value.rs

1use libquickjs_sys as q;
2
3use crate::{ExecutionError, JsValue, ValueError};
4
5use super::make_cstring;
6use crate::bindings::ContextWrapper;
7
8#[repr(i32)]
9#[derive(PartialEq, Eq, Clone, Copy, Debug)]
10pub enum JsTag {
11    // Used by C code as a marker.
12    // Not relevant for bindings.
13    // First = q::JS_TAG_FIRST,
14    Int = q::JS_TAG_INT,
15    Bool = q::JS_TAG_BOOL,
16    Null = q::JS_TAG_NULL,
17    Module = q::JS_TAG_MODULE,
18    Object = q::JS_TAG_OBJECT,
19    String = q::JS_TAG_STRING,
20    Symbol = q::JS_TAG_SYMBOL,
21    #[cfg(feature = "bigint")]
22    BigInt = q::JS_TAG_BIG_INT,
23    Float64 = q::JS_TAG_FLOAT64,
24    // BigFloat = q::JS_TAG_BIG_FLOAT,
25    Exception = q::JS_TAG_EXCEPTION,
26    Undefined = q::JS_TAG_UNDEFINED,
27    // BigDecimal = q::JS_TAG_BIG_DECIMAL,
28    CatchOffset = q::JS_TAG_CATCH_OFFSET,
29    Uninitialized = q::JS_TAG_UNINITIALIZED,
30    FunctionBytecode = q::JS_TAG_FUNCTION_BYTECODE,
31}
32
33impl JsTag {
34    #[inline]
35    pub(super) fn from_c(value: &q::JSValue) -> JsTag {
36        let inner = unsafe { q::JS_VALUE_GET_TAG(*value) };
37        match inner {
38            q::JS_TAG_INT => JsTag::Int,
39            q::JS_TAG_BOOL => JsTag::Bool,
40            q::JS_TAG_NULL => JsTag::Null,
41            q::JS_TAG_MODULE => JsTag::Module,
42            q::JS_TAG_OBJECT => JsTag::Object,
43            q::JS_TAG_STRING => JsTag::String,
44            q::JS_TAG_SYMBOL => JsTag::Symbol,
45            q::JS_TAG_FLOAT64 => JsTag::Float64,
46            // q::JS_TAG_BIG_FLOAT => JsTag::BigFloat,
47            q::JS_TAG_EXCEPTION => JsTag::Exception,
48            q::JS_TAG_UNDEFINED => JsTag::Undefined,
49            // q::JS_TAG_BIG_DECIMAL => JsTag::BigDecimal,
50            q::JS_TAG_CATCH_OFFSET => JsTag::CatchOffset,
51            q::JS_TAG_UNINITIALIZED => JsTag::Uninitialized,
52            q::JS_TAG_FUNCTION_BYTECODE => JsTag::FunctionBytecode,
53            #[cfg(feature = "bigint")]
54            q::JS_TAG_BIG_INT => JsTag::BigInt,
55            _other => {
56                unreachable!()
57            }
58        }
59    }
60
61    pub(super) fn to_c(self) -> i32 {
62        // TODO: figure out why this is needed
63        // Just casting with `as` does not work correctly
64        match self {
65            JsTag::Int => q::JS_TAG_INT,
66            JsTag::Bool => q::JS_TAG_BOOL,
67            JsTag::Null => q::JS_TAG_NULL,
68            JsTag::Module => q::JS_TAG_MODULE,
69            JsTag::Object => q::JS_TAG_OBJECT,
70            JsTag::String => q::JS_TAG_STRING,
71            JsTag::Symbol => q::JS_TAG_SYMBOL,
72            JsTag::Float64 => q::JS_TAG_FLOAT64,
73            // JsTag::BigFloat => q::JS_TAG_BIG_FLOAT,
74            JsTag::Exception => q::JS_TAG_EXCEPTION,
75            JsTag::Undefined => q::JS_TAG_UNDEFINED,
76            // JsTag::BigDecimal => q::JS_TAG_BIG_DECIMAL,
77            JsTag::CatchOffset => q::JS_TAG_CATCH_OFFSET,
78            JsTag::Uninitialized => q::JS_TAG_UNINITIALIZED,
79            JsTag::FunctionBytecode => q::JS_TAG_FUNCTION_BYTECODE,
80            #[cfg(feature = "bigint")]
81            JsTag::BigInt => q::JS_TAG_FUNCTION_BYTECODE,
82        }
83    }
84
85    /// Returns `true` if the js_tag is [`Undefined`].
86    #[inline]
87    pub fn is_undefined(&self) -> bool {
88        matches!(self, Self::Undefined)
89    }
90
91    /// Returns `true` if the js_tag is [`Object`].
92    #[inline]
93    pub fn is_object(&self) -> bool {
94        matches!(self, Self::Object)
95    }
96
97    /// Returns `true` if the js_tag is [`Exception`].
98    #[inline]
99    pub fn is_exception(&self) -> bool {
100        matches!(self, Self::Exception)
101    }
102
103    /// Returns `true` if the js_tag is [`Int`].
104    #[inline]
105    pub fn is_int(&self) -> bool {
106        matches!(self, Self::Int)
107    }
108
109    /// Returns `true` if the js_tag is [`Bool`].
110    #[inline]
111    pub fn is_bool(&self) -> bool {
112        matches!(self, Self::Bool)
113    }
114
115    /// Returns `true` if the js_tag is [`Null`].
116    #[inline]
117    pub fn is_null(&self) -> bool {
118        matches!(self, Self::Null)
119    }
120
121    /// Returns `true` if the js_tag is [`Module`].
122    #[inline]
123    pub fn is_module(&self) -> bool {
124        matches!(self, Self::Module)
125    }
126
127    /// Returns `true` if the js_tag is [`String`].
128    #[inline]
129    pub fn is_string(&self) -> bool {
130        matches!(self, Self::String)
131    }
132
133    /// Returns `true` if the js_tag is [`Symbol`].
134    #[inline]
135    pub fn is_symbol(&self) -> bool {
136        matches!(self, Self::Symbol)
137    }
138
139    /// Returns `true` if the js_tag is [`BigInt`].
140    #[cfg(feature = "bigint")]
141    #[inline]
142    pub fn is_big_int(&self) -> bool {
143        matches!(self, Self::BigInt)
144    }
145
146    /// Returns `true` if the js_tag is [`Float64`].
147    #[inline]
148    pub fn is_float64(&self) -> bool {
149        matches!(self, Self::Float64)
150    }
151
152    ///// Returns `true` if the js_tag is [`BigFloat`].
153    // #[inline]
154    // pub fn is_big_float(&self) -> bool {
155    //     matches!(self, Self::BigFloat)
156    // }
157
158    // /// Returns `true` if the js_tag is [`BigDecimal`].
159    // #[inline]
160    // pub fn is_big_decimal(&self) -> bool {
161    //     matches!(self, Self::BigDecimal)
162    // }
163}
164
165pub struct OwnedJsAtom<'a> {
166    context: &'a ContextWrapper,
167    value: q::JSAtom,
168}
169
170impl<'a> OwnedJsAtom<'a> {
171    #[inline]
172    pub fn new(context: &'a ContextWrapper, value: q::JSAtom) -> Self {
173        Self { context, value }
174    }
175}
176
177impl<'a> Drop for OwnedJsAtom<'a> {
178    fn drop(&mut self) {
179        unsafe {
180            q::JS_FreeAtom(self.context.context, self.value);
181        }
182    }
183}
184
185impl<'a> Clone for OwnedJsAtom<'a> {
186    fn clone(&self) -> Self {
187        unsafe { q::JS_DupAtom(self.context.context, self.value) };
188        Self {
189            context: self.context,
190            value: self.value,
191        }
192    }
193}
194
195/// OwnedJsValue wraps a Javascript value owned by the QuickJs runtime.
196///
197/// Guarantees cleanup of resources by dropping the value from the runtime.
198///
199/// ### Comparison to [`crate::JsValue`]:
200///
201/// `JsValue` is a native Rust value that can be converted to QuickJs native
202/// types. `OwnedJsValue`, in contrast, owns the underlying QuickJs runtime
203/// value directly.
204// TODO: provide usage docs.
205pub struct OwnedJsValue<'a> {
206    context: &'a ContextWrapper,
207    // FIXME: make private again, just for testing
208    pub(crate) value: q::JSValue,
209}
210
211impl<'a> OwnedJsValue<'a> {
212    #[inline]
213    pub(crate) fn context(&self) -> &ContextWrapper {
214        self.context
215    }
216
217    #[inline]
218    pub(crate) fn new(context: &'a ContextWrapper, value: q::JSValue) -> Self {
219        Self { context, value }
220    }
221
222    #[inline]
223    pub(crate) fn tag(&self) -> JsTag {
224        JsTag::from_c(&self.value)
225    }
226
227    /// Get the inner JSValue without increasing ref count.
228    ///
229    /// Unsafe because the caller must ensure proper memory management.
230    pub(super) unsafe fn as_inner(&self) -> &q::JSValue {
231        &self.value
232    }
233
234    /// Extract the underlying JSValue.
235    ///
236    /// Unsafe because the caller must ensure memory management. (eg JS_FreeValue)
237    pub(super) unsafe fn extract(self) -> q::JSValue {
238        let v = self.value;
239        std::mem::forget(self);
240        v
241    }
242
243    /// Check if this value is `null`.
244    #[inline]
245    pub fn is_null(&self) -> bool {
246        self.tag().is_null()
247    }
248
249    /// Check if this value is `undefined`.
250    #[inline]
251    pub fn is_undefined(&self) -> bool {
252        self.tag() == JsTag::Undefined
253    }
254
255    /// Check if this value is `bool`.
256    #[inline]
257    pub fn is_bool(&self) -> bool {
258        self.tag() == JsTag::Bool
259    }
260
261    /// Check if this value is a Javascript exception.
262    #[inline]
263    pub fn is_exception(&self) -> bool {
264        self.tag() == JsTag::Exception
265    }
266
267    /// Check if this value is a Javascript object.
268    #[inline]
269    pub fn is_object(&self) -> bool {
270        self.tag() == JsTag::Object
271    }
272
273    /// Check if this value is a Javascript array.
274    #[inline]
275    pub fn is_array(&self) -> bool {
276        unsafe { q::JS_IsArray(self.context.context, self.value) == 1 }
277    }
278
279    /// Check if this value is a Javascript function.
280    #[inline]
281    pub fn is_function(&self) -> bool {
282        unsafe { q::JS_IsFunction(self.context.context, self.value) }
283    }
284
285    /// Check if this value is a Javascript module.
286    #[inline]
287    pub fn is_module(&self) -> bool {
288        self.tag().is_module()
289    }
290
291    /// Check if this value is a Javascript string.
292    #[inline]
293    pub fn is_string(&self) -> bool {
294        self.tag() == JsTag::String
295    }
296
297    /// Check if this value is a bytecode compiled function.
298    #[inline]
299    pub fn is_compiled_function(&self) -> bool {
300        self.tag() == JsTag::FunctionBytecode
301    }
302
303    /// Serialize this value into a [`JsValue`].
304    pub fn to_value(&self) -> Result<JsValue, ValueError> {
305        self.context.to_value(&self.value)
306    }
307
308    pub(crate) fn to_bool(&self) -> Result<bool, ValueError> {
309        match self.to_value()? {
310            JsValue::Bool(b) => Ok(b),
311            _ => Err(ValueError::UnexpectedType),
312        }
313    }
314
315    pub(crate) fn try_into_object(self) -> Result<OwnedJsObject<'a>, ValueError> {
316        OwnedJsObject::try_from_value(self)
317    }
318
319    pub(crate) fn try_into_function(self) -> Result<JsFunction<'a>, ValueError> {
320        JsFunction::try_from_value(self)
321    }
322
323    pub(crate) fn try_into_compiled_function(self) -> Result<JsCompiledFunction<'a>, ValueError> {
324        JsCompiledFunction::try_from_value(self)
325    }
326
327    pub(crate) fn try_into_module(self) -> Result<JsModule<'a>, ValueError> {
328        JsModule::try_from_value(self)
329    }
330
331    /// Call the Javascript `.toString()` method on this value.
332    pub(crate) fn js_to_string(&self) -> Result<String, ExecutionError> {
333        let value = if self.is_string() {
334            self.to_value()?
335        } else {
336            let raw = unsafe { q::JS_ToString(self.context.context, self.value) };
337            let value = OwnedJsValue::new(self.context, raw);
338
339            if !value.is_string() {
340                return Err(ExecutionError::Exception(
341                    "Could not convert value to string".into(),
342                ));
343            }
344            value.to_value()?
345        };
346
347        Ok(value.as_str().unwrap().to_string())
348    }
349
350    #[cfg(test)]
351    pub(crate) fn get_ref_count(&self) -> i32 {
352        if self.value.tag < 0 {
353            // This transmute is OK since if tag < 0, the union will be a refcount
354            // pointer.
355            let ptr = unsafe { self.value.u.ptr as *mut q::JSRefCountHeader };
356            let pref: &mut q::JSRefCountHeader = &mut unsafe { *ptr };
357            pref.ref_count
358        } else {
359            -1
360        }
361    }
362}
363
364impl<'a> Drop for OwnedJsValue<'a> {
365    fn drop(&mut self) {
366        unsafe {
367            q::JS_FreeValue(self.context.context, self.value);
368        }
369    }
370}
371
372impl<'a> Clone for OwnedJsValue<'a> {
373    fn clone(&self) -> Self {
374        unsafe { q::JS_DupValue(self.context.context, self.value) };
375        Self {
376            context: self.context,
377            value: self.value,
378        }
379    }
380}
381
382impl<'a> std::fmt::Debug for OwnedJsValue<'a> {
383    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
384        write!(f, "{:?}(_)", self.tag())
385    }
386}
387
388pub struct OwnedJsArray<'a> {
389    value: OwnedJsValue<'a>,
390}
391
392impl<'a> OwnedJsArray<'a> {
393    pub fn new(value: OwnedJsValue<'a>) -> Option<Self> {
394        if value.is_array() {
395            Some(Self { value })
396        } else {
397            None
398        }
399    }
400}
401
402/// Wraps an object from the QuickJs runtime.
403/// Provides convenience property accessors.
404#[derive(Clone, Debug)]
405pub struct OwnedJsObject<'a> {
406    value: OwnedJsValue<'a>,
407}
408
409impl<'a> OwnedJsObject<'a> {
410    pub fn try_from_value(value: OwnedJsValue<'a>) -> Result<Self, ValueError> {
411        if !value.is_object() {
412            Err(ValueError::Internal("Expected an object".into()))
413        } else {
414            Ok(Self { value })
415        }
416    }
417
418    pub fn into_value(self) -> OwnedJsValue<'a> {
419        self.value
420    }
421
422    pub fn property(&self, name: &str) -> Result<Option<OwnedJsValue<'a>>, ExecutionError> {
423        // TODO: prevent allocation
424        let cname = make_cstring(name)?;
425        let value = {
426            let raw = unsafe {
427                q::JS_GetPropertyStr(self.value.context.context, self.value.value, cname.as_ptr())
428            };
429            OwnedJsValue::new(self.value.context, raw)
430        };
431        let tag = value.tag();
432
433        if tag.is_exception() {
434            Err(ExecutionError::Internal(format!(
435                "Exception while getting property '{}'",
436                name
437            )))
438        } else if tag.is_undefined() {
439            Ok(None)
440        } else {
441            Ok(Some(value))
442        }
443    }
444
445    pub fn property_require(&self, name: &str) -> Result<OwnedJsValue<'a>, ExecutionError> {
446        self.property(name)?
447            .ok_or_else(|| ExecutionError::Internal(format!("Property '{}' not found", name)))
448    }
449
450    /// Determine if the object is a promise by checking the presence of
451    /// a 'then' and a 'catch' property.
452    pub fn is_promise(&self) -> Result<bool, ExecutionError> {
453        if let Some(p) = self.property("then")? {
454            if p.is_function() {
455                return Ok(true);
456            }
457        }
458        if let Some(p) = self.property("catch")? {
459            if p.is_function() {
460                return Ok(true);
461            }
462        }
463        Ok(false)
464    }
465
466    pub fn set_property(&self, name: &str, value: OwnedJsValue<'a>) -> Result<(), ExecutionError> {
467        let cname = make_cstring(name)?;
468        unsafe {
469            // NOTE: SetPropertyStr takes ownership of the value.
470            // We do not, however, call OwnedJsValue::extract immediately, so
471            // the inner JSValue is still managed.
472            // `mem::forget` is called below only if SetProperty succeeds.
473            // This prevents leaks when an error occurs.
474            let ret = q::JS_SetPropertyStr(
475                self.value.context.context,
476                self.value.value,
477                cname.as_ptr(),
478                value.value,
479            );
480
481            if ret < 0 {
482                Err(ExecutionError::Exception("Could not set property".into()))
483            } else {
484                // Now we can call forget to prevent calling the destructor.
485                std::mem::forget(value);
486                Ok(())
487            }
488        }
489    }
490}
491
492/// Wraps an object from the QuickJs runtime.
493/// Provides convenience property accessors.
494#[derive(Clone, Debug)]
495pub struct JsFunction<'a> {
496    value: OwnedJsValue<'a>,
497}
498
499impl<'a> JsFunction<'a> {
500    pub fn try_from_value(value: OwnedJsValue<'a>) -> Result<Self, ValueError> {
501        if !value.is_function() {
502            Err(ValueError::Internal(format!(
503                "Expected a function, got {:?}",
504                value.tag()
505            )))
506        } else {
507            Ok(Self { value })
508        }
509    }
510
511    pub fn into_value(self) -> OwnedJsValue<'a> {
512        self.value
513    }
514
515    pub fn call(&self, args: Vec<OwnedJsValue<'a>>) -> Result<OwnedJsValue<'a>, ExecutionError> {
516        let mut qargs = args.iter().map(|arg| arg.value).collect::<Vec<_>>();
517
518        let qres_raw = unsafe {
519            q::JS_Call(
520                self.value.context.context,
521                self.value.value,
522                q::JSValue {
523                    u: q::JSValueUnion { int32: 0 },
524                    tag: JsTag::Null as i64,
525                },
526                qargs.len() as i32,
527                qargs.as_mut_ptr(),
528            )
529        };
530        Ok(OwnedJsValue::new(self.value.context, qres_raw))
531    }
532}
533
534/// A bytecode compiled function.
535#[derive(Clone, Debug)]
536pub struct JsCompiledFunction<'a> {
537    value: OwnedJsValue<'a>,
538}
539
540impl<'a> JsCompiledFunction<'a> {
541    pub(crate) fn try_from_value(value: OwnedJsValue<'a>) -> Result<Self, ValueError> {
542        if !value.is_compiled_function() {
543            Err(ValueError::Internal(format!(
544                "Expected a compiled function, got {:?}",
545                value.tag()
546            )))
547        } else {
548            Ok(Self { value })
549        }
550    }
551
552    pub(crate) fn as_value(&self) -> &OwnedJsValue<'_> {
553        &self.value
554    }
555
556    pub(crate) fn into_value(self) -> OwnedJsValue<'a> {
557        self.value
558    }
559
560    /// Evaluate this compiled function and return the resulting value.
561    // FIXME: add example
562    pub fn eval(&'a self) -> Result<OwnedJsValue<'a>, ExecutionError> {
563        super::compile::run_compiled_function(self)
564    }
565
566    /// Convert this compiled function into QuickJS bytecode.
567    ///
568    /// Bytecode can be stored and loaded with [`Context::compile`].
569    // FIXME: add example
570    pub fn to_bytecode(&self) -> Result<Vec<u8>, ExecutionError> {
571        Ok(super::compile::to_bytecode(self.value.context, self))
572    }
573}
574
575/// A bytecode compiled module.
576pub struct JsModule<'a> {
577    value: OwnedJsValue<'a>,
578}
579
580impl<'a> JsModule<'a> {
581    pub fn try_from_value(value: OwnedJsValue<'a>) -> Result<Self, ValueError> {
582        if !value.is_module() {
583            Err(ValueError::Internal(format!(
584                "Expected a compiled function, got {:?}",
585                value.tag()
586            )))
587        } else {
588            Ok(Self { value })
589        }
590    }
591
592    pub fn into_value(self) -> OwnedJsValue<'a> {
593        self.value
594    }
595}
596
597/// The result of loading QuickJs bytecode.
598/// Either a function or a module.
599pub enum JsCompiledValue<'a> {
600    Function(JsCompiledFunction<'a>),
601    Module(JsModule<'a>),
602}