Skip to main content

quickjs_runtime/
quickjsvalueadapter.rs

1//! JSValueRef is a wrapper for quickjs's JSValue. it provides automatic reference counting making it safer to use  
2
3use crate::jsutils::{JsError, JsValueType};
4use crate::quickjs_utils::typedarrays::is_typed_array;
5use crate::quickjs_utils::{arrays, errors, functions, primitives, promises};
6use crate::reflection::is_proxy_instance;
7use libquickjs_sys as q;
8use std::hash::{Hash, Hasher};
9use std::ptr::null_mut;
10
11#[allow(clippy::upper_case_acronyms)]
12pub struct QuickJsValueAdapter {
13    pub(crate) context: *mut q::JSContext,
14    value: q::JSValue,
15    ref_ct_decr_on_drop: bool,
16    label: String,
17}
18
19impl Hash for QuickJsValueAdapter {
20    fn hash<H: Hasher>(&self, state: &mut H) {
21        let self_u = self.value.u;
22        if self.is_i32() || self.is_bool() {
23            #[cfg(feature = "bellard")]
24            unsafe {
25                self_u.uint64.hash(state)
26            };
27            #[cfg(feature = "quickjs-ng")]
28            unsafe {
29                self_u.int32.hash(state)
30            };
31        } else if self.is_f64() {
32            unsafe { (self_u.float64 as i32).hash(state) };
33        } else {
34            unsafe { self_u.ptr.hash(state) };
35        }
36    }
37}
38
39impl PartialEq for QuickJsValueAdapter {
40    fn eq(&self, other: &Self) -> bool {
41        if self.get_tag() != other.get_tag() {
42            false
43        } else {
44            let self_u = self.value.u;
45            let other_u = other.value.u;
46            unsafe {
47                #[cfg(feature = "bellard")]
48                return self_u.uint64 == other_u.uint64
49                    && self_u.float64 == other_u.float64
50                    && self_u.ptr == other_u.ptr;
51                #[cfg(feature = "quickjs-ng")]
52                return self_u.int32 == other_u.int32
53                    && self_u.float64 == other_u.float64
54                    && self_u.ptr == other_u.ptr;
55            }
56        }
57    }
58}
59
60impl Eq for QuickJsValueAdapter {}
61
62impl QuickJsValueAdapter {
63    #[allow(dead_code)]
64    pub(crate) fn label(&mut self, label: &str) {
65        self.label = label.to_string()
66    }
67}
68
69impl Clone for QuickJsValueAdapter {
70    fn clone(&self) -> Self {
71        Self::new(
72            self.context,
73            self.value,
74            true,
75            true,
76            format!("clone of {}", self.label).as_str(),
77        )
78    }
79}
80
81impl Drop for QuickJsValueAdapter {
82    fn drop(&mut self) {
83        //log::debug!(
84        //    "dropping OwnedValueRef, before free: {}, ref_ct: {}, tag: {}",
85        //    self.label,
86        //    self.get_ref_count(),
87        //    self.value.tag
88        //);
89
90        // All tags < 0 are garbage collected and need to be freed.
91        if self.value.tag < 0 {
92            // This transmute is OK since if tag < 0, the union will be a refcount
93            // pointer.
94
95            if self.ref_ct_decr_on_drop {
96                #[cfg(feature = "bellard")]
97                if self.get_ref_count() <= 0 {
98                    log::error!(
99                        "dropping ref while refcount already 0, which is bad mmkay.. {}",
100                        self.label
101                    );
102                    panic!(
103                        "dropping ref while refcount already 0, which is bad mmkay.. {}",
104                        self.label
105                    );
106                }
107                self.decrement_ref_count();
108            }
109        }
110        //log::trace!("dropping OwnedValueRef, after free",);
111    }
112}
113
114impl std::fmt::Debug for QuickJsValueAdapter {
115    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
116        match self.value.tag {
117            TAG_EXCEPTION => write!(f, "Exception(?)"),
118            TAG_NULL => write!(f, "NULL"),
119            TAG_UNDEFINED => write!(f, "UNDEFINED"),
120            TAG_BOOL => write!(f, "Bool(?)",),
121            TAG_INT => write!(f, "Int(?)"),
122            TAG_FLOAT64 => write!(f, "Float(?)"),
123            TAG_STRING => write!(f, "String(?)"),
124            #[cfg(feature = "bellard")]
125            TAG_STRING_ROPE => write!(f, "String(?)"),
126            TAG_OBJECT => write!(f, "Object(?)"),
127            TAG_MODULE => write!(f, "Module(?)"),
128            TAG_BIG_INT => write!(f, "BigInt(?)"),
129            _ => write!(f, "Unknown Tag(?)"),
130        }
131    }
132}
133
134impl QuickJsValueAdapter {
135    pub(crate) fn increment_ref_count(&self) {
136        if self.get_tag() < 0 {
137            unsafe {
138                #[allow(clippy::let_unit_value)]
139                let _ = libquickjs_sys::JS_DupValue(self.context, *self.borrow_value());
140            }
141        }
142    }
143
144    pub(crate) fn decrement_ref_count(&self) {
145        if self.get_tag() < 0 {
146            unsafe { libquickjs_sys::JS_FreeValue(self.context, *self.borrow_value()) }
147        }
148    }
149
150    pub fn get_tag(&self) -> i64 {
151        self.value.tag
152    }
153
154    pub fn new_no_context(value: q::JSValue, label: &str) -> Self {
155        Self {
156            context: null_mut(),
157            value,
158            ref_ct_decr_on_drop: false,
159            label: label.to_string(),
160        }
161    }
162
163    pub fn new(
164        context: *mut q::JSContext,
165        value: q::JSValue,
166        ref_ct_incr: bool,
167        ref_ct_decr_on_drop: bool,
168        label: &str,
169    ) -> Self {
170        debug_assert!(!label.is_empty());
171
172        let s = Self {
173            context,
174            value,
175            ref_ct_decr_on_drop,
176            label: label.to_string(),
177        };
178        if ref_ct_incr {
179            s.increment_ref_count();
180        }
181        s
182    }
183
184    #[cfg(feature = "bellard")]
185    pub fn get_ref_count(&self) -> i32 {
186        if self.get_tag() < 0 {
187            unsafe { q::JS_ValueGetRefCount(*self.borrow_value()) }
188        } else {
189            -1
190        }
191    }
192
193    /// borrow the value but first increment the refcount, this is useful for when the value is returned or passed to functions
194    pub fn clone_value_incr_rc(&self) -> q::JSValue {
195        self.increment_ref_count();
196        self.value
197    }
198
199    pub fn borrow_value(&self) -> &q::JSValue {
200        &self.value
201    }
202
203    pub fn borrow_value_mut(&mut self) -> &mut q::JSValue {
204        &mut self.value
205    }
206
207    pub fn is_null_or_undefined(&self) -> bool {
208        self.is_null() || self.is_undefined()
209    }
210
211    /// return true if the wrapped value represents a JS null value
212    pub fn is_undefined(&self) -> bool {
213        unsafe { q::JS_IsUndefined(self.value) }
214    }
215
216    /// return true if the wrapped value represents a JS null value
217    pub fn is_null(&self) -> bool {
218        unsafe { q::JS_IsNull(self.value) }
219    }
220
221    /// return true if the wrapped value represents a JS boolean value
222    pub fn is_bool(&self) -> bool {
223        unsafe { q::JS_IsBool(self.value) }
224    }
225
226    /// return true if the wrapped value represents a JS INT value
227    pub fn is_i32(&self) -> bool {
228        // todo figure out diff between i32/f64/Number
229        // unsafe { q::JS_IsNumber(self.borrow_value()) }
230        self.borrow_value().tag == TAG_INT
231    }
232
233    /// return true if the wrapped value represents a Module
234    pub fn is_module(&self) -> bool {
235        self.borrow_value().tag == TAG_MODULE
236    }
237
238    /// return true if the wrapped value represents a compiled function
239    pub fn is_compiled_function(&self) -> bool {
240        self.borrow_value().tag == TAG_FUNCTION_BYTECODE
241    }
242
243    /// return true if the wrapped value represents a JS F64 value
244    pub fn is_f64(&self) -> bool {
245        self.borrow_value().tag == TAG_FLOAT64
246    }
247
248    pub fn is_big_int(&self) -> bool {
249        // unsafe { q::JS_IsBigInt(ctx, self.borrow_value()) }
250        self.borrow_value().tag == TAG_BIG_INT || self.borrow_value().tag == TAG_SHORT_BIG_INT
251    }
252
253    /// return true if the wrapped value represents a JS Exception value
254    pub fn is_exception(&self) -> bool {
255        unsafe { q::JS_IsException(self.value) }
256    }
257
258    /// return true if the wrapped value represents a JS Object value
259    pub fn is_object(&self) -> bool {
260        unsafe { q::JS_IsObject(self.value) }
261    }
262
263    /// return true if the wrapped value represents a JS String value
264    pub fn is_string(&self) -> bool {
265        unsafe { q::JS_IsString(self.value) }
266    }
267}
268
269pub(crate) const TAG_BIG_INT: i64 = libquickjs_sys::JS_TAG_BIG_INT as i64;
270pub(crate) const TAG_SHORT_BIG_INT: i64 = libquickjs_sys::JS_TAG_SHORT_BIG_INT as i64;
271
272pub(crate) const TAG_STRING: i64 = libquickjs_sys::JS_TAG_STRING as i64;
273
274pub(crate) const TAG_STRING_ROPE: i64 = libquickjs_sys::JS_TAG_STRING_ROPE as i64;
275
276pub(crate) const TAG_MODULE: i64 = libquickjs_sys::JS_TAG_MODULE as i64;
277pub(crate) const TAG_FUNCTION_BYTECODE: i64 = libquickjs_sys::JS_TAG_FUNCTION_BYTECODE as i64;
278pub(crate) const TAG_OBJECT: i64 = libquickjs_sys::JS_TAG_OBJECT as i64;
279pub(crate) const TAG_INT: i64 = libquickjs_sys::JS_TAG_INT as i64;
280pub(crate) const TAG_BOOL: i64 = libquickjs_sys::JS_TAG_BOOL as i64;
281pub(crate) const TAG_NULL: i64 = libquickjs_sys::JS_TAG_NULL as i64;
282pub(crate) const TAG_UNDEFINED: i64 = libquickjs_sys::JS_TAG_UNDEFINED as i64;
283pub(crate) const TAG_EXCEPTION: i64 = libquickjs_sys::JS_TAG_EXCEPTION as i64;
284pub(crate) const TAG_FLOAT64: i64 = libquickjs_sys::JS_TAG_FLOAT64 as i64;
285
286impl QuickJsValueAdapter {
287    pub fn is_function(&self) -> bool {
288        self.is_object() && self.get_js_type() == JsValueType::Function
289    }
290    pub fn is_array(&self) -> bool {
291        self.is_object() && self.get_js_type() == JsValueType::Array
292    }
293    pub fn is_error(&self) -> bool {
294        self.is_object() && self.get_js_type() == JsValueType::Error
295    }
296    pub fn is_promise(&self) -> bool {
297        self.is_object() && self.get_js_type() == JsValueType::Promise
298    }
299
300    pub fn get_js_type(&self) -> JsValueType {
301        match self.get_tag() {
302            TAG_EXCEPTION => JsValueType::Error,
303            TAG_NULL => JsValueType::Null,
304            TAG_UNDEFINED => JsValueType::Undefined,
305            TAG_BOOL => JsValueType::Boolean,
306            TAG_INT => JsValueType::I32,
307            TAG_FLOAT64 => JsValueType::F64,
308            TAG_STRING => JsValueType::String,
309            TAG_STRING_ROPE => JsValueType::String,
310            TAG_OBJECT => {
311                // todo get classProto.name and match
312                // if bellard, match on classid
313                if unsafe { functions::is_function(self.context, self) } {
314                    JsValueType::Function
315                } else if unsafe { errors::is_error(self.context, self) } {
316                    JsValueType::Error
317                } else if unsafe { arrays::is_array(self.context, self) } {
318                    JsValueType::Array
319                } else if unsafe { promises::is_promise(self.context, self) } {
320                    JsValueType::Promise
321                } else {
322                    JsValueType::Object
323                }
324            }
325            TAG_BIG_INT | TAG_SHORT_BIG_INT => JsValueType::BigInt,
326            TAG_MODULE => todo!(),
327            _ => JsValueType::Undefined,
328        }
329    }
330
331    pub fn is_typed_array(&self) -> bool {
332        self.is_object() && unsafe { is_typed_array(self.context, self) }
333    }
334
335    pub fn is_proxy_instance(&self) -> bool {
336        self.is_object() && unsafe { is_proxy_instance(self.context, self) }
337    }
338
339    pub fn type_of(&self) -> &'static str {
340        match self.get_tag() {
341            TAG_BIG_INT => "bigint",
342            TAG_STRING => "string",
343            TAG_STRING_ROPE => "string",
344            TAG_MODULE => "module",
345            TAG_FUNCTION_BYTECODE => "function",
346            TAG_OBJECT => {
347                if self.get_js_type() == JsValueType::Function {
348                    "function"
349                } else {
350                    "object"
351                }
352            }
353            TAG_INT => "number",
354            TAG_BOOL => "boolean",
355            TAG_NULL => "object",
356            TAG_UNDEFINED => "undefined",
357            TAG_EXCEPTION => "object",
358            TAG_FLOAT64 => "number",
359            _ => "unknown",
360        }
361    }
362
363    pub fn to_bool(&self) -> bool {
364        if self.get_js_type() == JsValueType::Boolean {
365            primitives::to_bool(self).expect("could not convert bool to bool")
366        } else {
367            panic!("not a boolean");
368        }
369    }
370
371    pub fn to_i32(&self) -> i32 {
372        if self.get_js_type() == JsValueType::I32 {
373            primitives::to_i32(self).expect("could not convert to i32")
374        } else {
375            panic!("not an i32");
376        }
377    }
378
379    pub fn to_f64(&self) -> f64 {
380        if self.get_js_type() == JsValueType::F64 {
381            primitives::to_f64(self).expect("could not convert to f64")
382        } else {
383            panic!("not a f64");
384        }
385    }
386
387    pub fn to_string(&self) -> Result<String, JsError> {
388        match self.get_js_type() {
389            JsValueType::I32 => Ok(self.to_i32().to_string()),
390            JsValueType::F64 => Ok(self.to_f64().to_string()),
391            JsValueType::String => unsafe { primitives::to_string(self.context, self) },
392            JsValueType::Boolean => {
393                if self.to_bool() {
394                    Ok("true".to_string())
395                } else {
396                    Ok("false".to_string())
397                }
398            }
399            JsValueType::Error => {
400                let js_error = unsafe { errors::error_to_js_error(self.context, self) };
401                Ok(format!("{js_error}"))
402            }
403            _ => unsafe { functions::call_to_string(self.context, self) },
404        }
405    }
406}
407
408#[cfg(test)]
409pub mod tests {
410    use crate::facades::tests::init_test_rt;
411    use crate::jsutils::{JsValueType, Script};
412
413    #[test]
414    fn test_to_str() {
415        let rt = init_test_rt();
416        rt.exe_rt_task_in_event_loop(|q_js_rt| {
417            let q_ctx = q_js_rt.get_main_realm();
418            let res = q_ctx.eval(Script::new("test_to_str.es", "('hello ' + 'world');"));
419
420            match res {
421                Ok(res) => {
422                    log::info!("script ran ok: {:?}", res);
423                    assert!(res.get_js_type() == JsValueType::String);
424                    assert_eq!(res.to_string().expect("str conv failed"), "hello world");
425                }
426                Err(e) => {
427                    log::error!("script failed: {}", e);
428                    panic!("script failed");
429                }
430            }
431        });
432    }
433}