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