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
266pub(crate) const TAG_STRING_ROPE: i64 = libquickjs_sys::JS_TAG_STRING_ROPE as i64;
267
268pub(crate) const TAG_MODULE: i64 = libquickjs_sys::JS_TAG_MODULE as i64;
269pub(crate) const TAG_FUNCTION_BYTECODE: i64 = libquickjs_sys::JS_TAG_FUNCTION_BYTECODE as i64;
270pub(crate) const TAG_OBJECT: i64 = libquickjs_sys::JS_TAG_OBJECT as i64;
271pub(crate) const TAG_INT: i64 = libquickjs_sys::JS_TAG_INT as i64;
272pub(crate) const TAG_BOOL: i64 = libquickjs_sys::JS_TAG_BOOL as i64;
273pub(crate) const TAG_NULL: i64 = libquickjs_sys::JS_TAG_NULL as i64;
274pub(crate) const TAG_UNDEFINED: i64 = libquickjs_sys::JS_TAG_UNDEFINED as i64;
275pub(crate) const TAG_EXCEPTION: i64 = libquickjs_sys::JS_TAG_EXCEPTION as i64;
276pub(crate) const TAG_FLOAT64: i64 = libquickjs_sys::JS_TAG_FLOAT64 as i64;
277
278impl QuickJsValueAdapter {
279    pub fn is_function(&self) -> bool {
280        self.is_object() && self.get_js_type() == JsValueType::Function
281    }
282    pub fn is_array(&self) -> bool {
283        self.is_object() && self.get_js_type() == JsValueType::Array
284    }
285    pub fn is_error(&self) -> bool {
286        self.is_object() && self.get_js_type() == JsValueType::Error
287    }
288    pub fn is_promise(&self) -> bool {
289        self.is_object() && self.get_js_type() == JsValueType::Promise
290    }
291
292    pub fn get_js_type(&self) -> JsValueType {
293        match self.get_tag() {
294            TAG_EXCEPTION => JsValueType::Error,
295            TAG_NULL => JsValueType::Null,
296            TAG_UNDEFINED => JsValueType::Undefined,
297            TAG_BOOL => JsValueType::Boolean,
298            TAG_INT => JsValueType::I32,
299            TAG_FLOAT64 => JsValueType::F64,
300            TAG_STRING => JsValueType::String,
301            TAG_STRING_ROPE => JsValueType::String,
302            TAG_OBJECT => {
303                // todo get classProto.name and match
304                // if bellard, match on classid
305                if unsafe { functions::is_function(self.context, self) } {
306                    JsValueType::Function
307                } else if unsafe { errors::is_error(self.context, self) } {
308                    JsValueType::Error
309                } else if unsafe { arrays::is_array(self.context, self) } {
310                    JsValueType::Array
311                } else if unsafe { promises::is_promise(self.context, self) } {
312                    JsValueType::Promise
313                } else {
314                    JsValueType::Object
315                }
316            }
317            TAG_BIG_INT | TAG_SHORT_BIG_INT => JsValueType::BigInt,
318            TAG_MODULE => todo!(),
319            _ => JsValueType::Undefined,
320        }
321    }
322
323    pub fn is_typed_array(&self) -> bool {
324        self.is_object() && unsafe { is_typed_array(self.context, self) }
325    }
326
327    pub fn is_proxy_instance(&self) -> bool {
328        self.is_object() && unsafe { is_proxy_instance(self.context, self) }
329    }
330
331    pub fn type_of(&self) -> &'static str {
332        match self.get_tag() {
333            TAG_BIG_INT => "bigint",
334            TAG_STRING => "string",
335            #[cfg(feature = "bellard")]
336            TAG_STRING_ROPE => "string",
337            TAG_MODULE => "module",
338            TAG_FUNCTION_BYTECODE => "function",
339            TAG_OBJECT => {
340                if self.get_js_type() == JsValueType::Function {
341                    "function"
342                } else {
343                    "object"
344                }
345            }
346            TAG_INT => "number",
347            TAG_BOOL => "boolean",
348            TAG_NULL => "object",
349            TAG_UNDEFINED => "undefined",
350            TAG_EXCEPTION => "object",
351            TAG_FLOAT64 => "number",
352            _ => "unknown",
353        }
354    }
355
356    pub fn to_bool(&self) -> bool {
357        if self.get_js_type() == JsValueType::Boolean {
358            primitives::to_bool(self).expect("could not convert bool to bool")
359        } else {
360            panic!("not a boolean");
361        }
362    }
363
364    pub fn to_i32(&self) -> i32 {
365        if self.get_js_type() == JsValueType::I32 {
366            primitives::to_i32(self).expect("could not convert to i32")
367        } else {
368            panic!("not an i32");
369        }
370    }
371
372    pub fn to_f64(&self) -> f64 {
373        if self.get_js_type() == JsValueType::F64 {
374            primitives::to_f64(self).expect("could not convert to f64")
375        } else {
376            panic!("not a f64");
377        }
378    }
379
380    pub fn to_string(&self) -> Result<String, JsError> {
381        match self.get_js_type() {
382            JsValueType::I32 => Ok(self.to_i32().to_string()),
383            JsValueType::F64 => Ok(self.to_f64().to_string()),
384            JsValueType::String => unsafe { primitives::to_string(self.context, self) },
385            JsValueType::Boolean => {
386                if self.to_bool() {
387                    Ok("true".to_string())
388                } else {
389                    Ok("false".to_string())
390                }
391            }
392            JsValueType::Error => {
393                let js_error = unsafe { errors::error_to_js_error(self.context, self) };
394                Ok(format!("{js_error}"))
395            }
396            _ => unsafe { functions::call_to_string(self.context, self) },
397        }
398    }
399}
400
401#[cfg(test)]
402pub mod tests {
403    use crate::facades::tests::init_test_rt;
404    use crate::jsutils::{JsValueType, Script};
405
406    #[test]
407    fn test_to_str() {
408        let rt = init_test_rt();
409        rt.exe_rt_task_in_event_loop(|q_js_rt| {
410            let q_ctx = q_js_rt.get_main_realm();
411            let res = q_ctx.eval(Script::new("test_to_str.es", "('hello ' + 'world');"));
412
413            match res {
414                Ok(res) => {
415                    log::info!("script ran ok: {:?}", res);
416                    assert!(res.get_js_type() == JsValueType::String);
417                    assert_eq!(res.to_string().expect("str conv failed"), "hello world");
418                }
419                Err(e) => {
420                    log::error!("script failed: {}", e);
421                    panic!("script failed");
422                }
423            }
424        });
425    }
426}