deft_quick_js/bindings/
convert.rs

1use std::{collections::HashMap, os::raw::c_char};
2use std::os::raw::{c_int, c_void};
3use std::ptr::null_mut;
4
5use libquickjs_sys as q;
6
7use crate::{JsValue, RawJSValue, ResourceValue, ValueError};
8
9use super::{droppable_value::DroppableValue, JsClass, make_cstring, Resource, ResourceObject};
10
11use super::{
12    TAG_BOOL, TAG_EXCEPTION, TAG_FLOAT64, TAG_INT, TAG_NULL, TAG_OBJECT, TAG_STRING, TAG_UNDEFINED,
13};
14
15#[cfg(feature = "bigint")]
16use {
17    super::TAG_BIG_INT,
18    crate::value::bigint::{BigInt, BigIntOrI64},
19};
20use libquickjs_sys::{JS_GetClassID, JS_GetOpaque, JS_GetOpaque2, JS_NewClass, JS_NewClassID, JS_NewObjectClass, JS_SetOpaque, JSClassDef, JSRuntime, JSValue, JS_VALUE_GET_TAG};
21
22#[cfg(feature = "chrono")]
23fn js_date_constructor(context: *mut q::JSContext) -> q::JSValue {
24    let global = unsafe { q::JS_GetGlobalObject(context) };
25    assert!(q::JS_IsObject(global));
26
27    let date_constructor = unsafe {
28        q::JS_GetPropertyStr(
29            context,
30            global,
31            std::ffi::CStr::from_bytes_with_nul(b"Date\0")
32                .unwrap()
33                .as_ptr(),
34        )
35    };
36    assert!(q::JS_IsObject(date_constructor));
37    unsafe { q::JS_FreeValue(context, global) };
38    date_constructor
39}
40
41#[cfg(feature = "bigint")]
42fn js_create_bigint_function(context: *mut q::JSContext) -> q::JSValue {
43    let global = unsafe { q::JS_GetGlobalObject(context) };
44    assert_eq!(global.tag, TAG_OBJECT);
45
46    let bigint_function = unsafe {
47        q::JS_GetPropertyStr(
48            context,
49            global,
50            std::ffi::CStr::from_bytes_with_nul(b"BigInt\0")
51                .unwrap()
52                .as_ptr(),
53        )
54    };
55    assert_eq!(bigint_function.tag, TAG_OBJECT);
56    unsafe { q::JS_FreeValue(context, global) };
57    bigint_function
58}
59
60/// Serialize a Rust value into a quickjs runtime value.
61//TODO pub(super)?
62pub fn serialize_value(
63    context: *mut q::JSContext,
64    value: JsValue,
65) -> Result<q::JSValue, ValueError> {
66    let v = match value {
67        JsValue::Undefined => q::JS_UNDEFINED,
68        JsValue::Null => q::JS_NULL,
69        JsValue::Bool(flag) => q::JS_MKVAL(q::JS_TAG_BOOL, if flag { 1 } else { 0 }),
70        JsValue::Int(val) => q::JS_MKVAL(q::JS_TAG_INT, val),
71        JsValue::Float(val) => q::__JS_NewFloat64(val),
72        JsValue::String(val) => {
73            let qval = unsafe {
74                q::JS_NewStringLen(context, val.as_ptr() as *const c_char, val.len() as _)
75            };
76
77            if q::JS_IsException(qval) {
78                return Err(ValueError::Internal(
79                    "Could not create string in runtime".into(),
80                ));
81            }
82
83            qval
84        }
85        JsValue::Array(values) => {
86            // Allocate a new array in the runtime.
87            let arr = unsafe { q::JS_NewArray(context) };
88            if q::JS_IsException(arr) {
89                return Err(ValueError::Internal(
90                    "Could not create array in runtime".into(),
91                ));
92            }
93
94            for (index, value) in values.into_iter().enumerate() {
95                let qvalue = match serialize_value(context, value) {
96                    Ok(qval) => qval,
97                    Err(e) => {
98                        // Make sure to free the array if a individual element
99                        // fails.
100
101                        unsafe {
102                            q::JS_FreeValue(context, arr);
103                        }
104
105                        return Err(e);
106                    }
107                };
108
109                let ret = unsafe {
110                    q::JS_DefinePropertyValueUint32(
111                        context,
112                        arr,
113                        index as u32,
114                        qvalue,
115                        q::JS_PROP_C_W_E as i32,
116                    )
117                };
118                if ret < 0 {
119                    // Make sure to free the array if a individual
120                    // element fails.
121                    unsafe {
122                        q::JS_FreeValue(context, arr);
123                    }
124                    return Err(ValueError::Internal(
125                        "Could not append element to array".into(),
126                    ));
127                }
128            }
129            arr
130        }
131        JsValue::Object(map) => {
132            let obj = unsafe { q::JS_NewObject(context) };
133            if q::JS_IsException(obj) {
134                return Err(ValueError::Internal("Could not create object".into()));
135            }
136
137            for (key, value) in map {
138                let ckey = make_cstring(key)?;
139
140                let qvalue = serialize_value(context, value).map_err(|e| {
141                    // Free the object if a property failed.
142                    unsafe {
143                        q::JS_FreeValue(context, obj);
144                    }
145                    e
146                })?;
147
148                let ret = unsafe {
149                    q::JS_DefinePropertyValueStr(
150                        context,
151                        obj,
152                        ckey.as_ptr(),
153                        qvalue,
154                        q::JS_PROP_C_W_E as i32,
155                    )
156                };
157                if ret < 0 {
158                    // Free the object if a property failed.
159                    unsafe {
160                        q::JS_FreeValue(context, obj);
161                    }
162                    return Err(ValueError::Internal(
163                        "Could not add add property to object".into(),
164                    ));
165                }
166            }
167
168            obj
169        }
170        JsValue::Raw(raw) => {
171            unsafe {
172                raw.create_js_value()
173            }
174        }
175        JsValue::Exception(raw) => {
176            unsafe {
177                raw.create_js_value()
178            }
179        }
180        JsValue::Resource(raw) => {
181            create_resource(context, raw)
182        }
183        #[cfg(feature = "chrono")]
184        JsValue::Date(datetime) => {
185            let date_constructor = js_date_constructor(context);
186
187            let f = datetime.timestamp_millis() as f64;
188
189            let timestamp = q::JS_NewFloat64(f);
190
191            let mut args = vec![timestamp];
192
193            let value = unsafe {
194                q::JS_CallConstructor(
195                    context,
196                    date_constructor,
197                    args.len() as i32,
198                    args.as_mut_ptr(),
199                )
200            };
201            unsafe {
202                q::JS_FreeValue(context, date_constructor);
203            }
204
205            if !q::JS_IsObject(value) {
206                return Err(ValueError::Internal(
207                    "Could not construct Date object".into(),
208                ));
209            }
210            value
211        }
212        #[cfg(feature = "bigint")]
213        JsValue::BigInt(int) => match int.inner {
214            BigIntOrI64::Int(int) => unsafe { q::JS_NewBigInt64(context, int) },
215            BigIntOrI64::BigInt(bigint) => {
216                let bigint_string = bigint.to_str_radix(10);
217                let s = unsafe {
218                    q::JS_NewStringLen(
219                        context,
220                        bigint_string.as_ptr() as *const c_char,
221                        bigint_string.len() as q::size_t,
222                    )
223                };
224                let s = DroppableValue::new(s, |&mut s| unsafe {
225                    q::JS_FreeValue(context, s);
226                });
227                if (*s).tag != TAG_STRING {
228                    return Err(ValueError::Internal(
229                        "Could not construct String object needed to create BigInt object".into(),
230                    ));
231                }
232
233                let mut args = vec![*s];
234
235                let bigint_function = js_create_bigint_function(context);
236                let bigint_function =
237                    DroppableValue::new(bigint_function, |&mut bigint_function| unsafe {
238                        q::JS_FreeValue(context, bigint_function);
239                    });
240                let js_bigint = unsafe {
241                    q::JS_Call(
242                        context,
243                        *bigint_function,
244                        q::JSValue {
245                            u: q::JSValueUnion { int32: 0 },
246                            tag: TAG_NULL,
247                        },
248                        1,
249                        args.as_mut_ptr(),
250                    )
251                };
252
253                if js_bigint.tag != TAG_BIG_INT {
254                    return Err(ValueError::Internal(
255                        "Could not construct BigInt object".into(),
256                    ));
257                }
258
259                js_bigint
260            }
261        },
262        JsValue::__NonExhaustive => unreachable!(),
263    };
264    Ok(v)
265}
266
267pub fn create_resource(context: *mut q::JSContext, resource: ResourceValue) -> JSValue {
268    unsafe  {
269        let class_id = Resource::class_id();
270        if class_id.id.get() == 0 {
271            let runtime = q::JS_GetRuntime(context);
272            let mut cls_id = 0;
273            JS_NewClassID(runtime, &mut cls_id);
274            class_id.id.set(cls_id);
275            extern fn finalizer(rt: *mut JSRuntime, val: JSValue) {
276                //println!("finalizer calling");
277                unsafe {
278                    let cls_id = JS_GetClassID(val);
279                    let opaque = JS_GetOpaque(val, cls_id) as *mut ResourceObject;
280                    let _ = Box::from_raw(opaque);
281                }
282                //println!("finalizer called");
283            }
284            let cls_def = JSClassDef {
285                class_name: Resource::NAME.as_ptr() as *const std::ffi::c_char,
286                finalizer: Some(finalizer),
287                gc_mark: None,
288                call: None,
289                exotic: null_mut(),
290            };
291            JS_NewClass(runtime, cls_id, &cls_def);
292        }
293
294        let class_id = class_id.id.get();
295        let res = JS_NewObjectClass(context, class_id as c_int);
296        let opaque = Box::into_raw(Box::new(ResourceObject {
297            data: resource,
298        }));
299        JS_SetOpaque(res, opaque as *mut c_void);
300        res
301    }
302
303}
304
305fn deserialize_array(
306    context: *mut q::JSContext,
307    raw_value: &q::JSValue,
308) -> Result<JsValue, ValueError> {
309    assert!(q::JS_IsObject(*raw_value));
310
311    let length_name = make_cstring("length")?;
312
313    let len_raw = unsafe { q::JS_GetPropertyStr(context, *raw_value, length_name.as_ptr()) };
314
315    let len_res = deserialize_value(context, &len_raw);
316    unsafe { q::JS_FreeValue(context, len_raw) };
317    let len = match len_res? {
318        JsValue::Int(x) => x,
319        _ => {
320            return Err(ValueError::Internal(
321                "Could not determine array length".into(),
322            ));
323        }
324    };
325
326    let mut values = Vec::new();
327    for index in 0..(len as usize) {
328        let value_raw = unsafe { q::JS_GetPropertyUint32(context, *raw_value, index as u32) };
329        if q::JS_IsException(value_raw) {
330            return Err(ValueError::Internal("Could not build array".into()));
331        }
332        let value_res = deserialize_value(context, &value_raw);
333        unsafe { q::JS_FreeValue(context, value_raw) };
334
335        let value = value_res?;
336        values.push(value);
337    }
338
339    Ok(JsValue::Array(values))
340}
341
342pub fn deserialize_object(context: *mut q::JSContext, obj: &q::JSValue) -> Result<HashMap<String, JsValue>, ValueError> {
343    assert_eq!(JS_VALUE_GET_TAG(*obj), q::JS_TAG_OBJECT);
344
345    let mut properties: *mut q::JSPropertyEnum = std::ptr::null_mut();
346    let mut count: u32 = 0;
347
348    let flags = (q::JS_GPN_STRING_MASK | q::JS_GPN_SYMBOL_MASK | q::JS_GPN_ENUM_ONLY) as i32;
349    let ret =
350        unsafe { q::JS_GetOwnPropertyNames(context, &mut properties, &mut count, *obj, flags) };
351    if ret != 0 {
352        return Err(ValueError::Internal(
353            "Could not get object properties".into(),
354        ));
355    }
356
357    // TODO: refactor into a more Rust-idiomatic iterator wrapper.
358    let properties = DroppableValue::new(properties, |&mut properties| {
359        for index in 0..count {
360            let prop = unsafe { properties.offset(index as isize) };
361            unsafe {
362                q::JS_FreeAtom(context, (*prop).atom);
363            }
364        }
365        unsafe {
366            q::js_free(context, properties as *mut std::ffi::c_void);
367        }
368    });
369
370    let mut map = HashMap::new();
371    for index in 0..count {
372        let prop = unsafe { (*properties).offset(index as isize) };
373        let raw_value = unsafe { q::JS_GetProperty(context, *obj, (*prop).atom) };
374        if q::JS_IsException(raw_value) {
375            return Err(ValueError::Internal("Could not get object property".into()));
376        }
377
378        let value_res = deserialize_value(context, &raw_value);
379        unsafe {
380            q::JS_FreeValue(context, raw_value);
381        }
382        let value = value_res?;
383
384        let key_value = unsafe { q::JS_AtomToString(context, (*prop).atom) };
385        if q::JS_IsException(key_value) {
386            return Err(ValueError::Internal(
387                "Could not get object property name".into(),
388            ));
389        }
390
391        let key_res = deserialize_value(context, &key_value);
392        unsafe {
393            q::JS_FreeValue(context, key_value);
394        }
395        let key = match key_res? {
396            JsValue::String(s) => s,
397            _ => {
398                return Err(ValueError::Internal("Could not get property name".into()));
399            }
400        };
401        map.insert(key, value);
402    }
403
404    // Ok(JsValue::Object(map))
405    Ok(map)
406}
407
408pub fn deserialize_value(
409    context: *mut q::JSContext,
410    value: &q::JSValue,
411) -> Result<JsValue, ValueError> {
412    let r = value;
413
414    match q::JS_VALUE_GET_TAG(*r) {
415        // Int.
416        q::JS_TAG_INT => {
417            let val = unsafe { q::JS_VALUE_GET_INT(*r) };
418            Ok(JsValue::Int(val))
419        }
420        // Bool.
421        q::JS_TAG_BOOL => {
422            let val = unsafe { q::JS_VALUE_GET_BOOL(*r) };
423            Ok(JsValue::Bool(val))
424        }
425        // Null.
426        q::JS_TAG_NULL => Ok(JsValue::Null),
427        // Undefined.
428        q::JS_TAG_UNDEFINED => Ok(JsValue::Undefined),
429        // Float.
430        q::JS_TAG_FLOAT64 => {
431            let val = unsafe { q::JS_VALUE_GET_FLOAT64(*r) };
432            Ok(JsValue::Float(val))
433        }
434        // String.
435        q::JS_TAG_STRING => {
436            let ptr = unsafe { q::JS_ToCStringLen2(context, std::ptr::null_mut(), *r, false) };
437
438            if ptr.is_null() {
439                return Err(ValueError::Internal(
440                    "Could not convert string: got a null pointer".into(),
441                ));
442            }
443
444            let cstr = unsafe { std::ffi::CStr::from_ptr(ptr) };
445
446            let s = cstr
447                .to_str()
448                .map_err(ValueError::InvalidString)?
449                .to_string();
450
451            // Free the c string.
452            unsafe { q::JS_FreeCString(context, ptr) };
453
454            Ok(JsValue::String(s))
455        }
456        // Object.
457        q::JS_TAG_OBJECT => {
458            let is_func = unsafe { q::JS_IsFunction(context, *r)};
459            if is_func {
460                //TODO remove
461                let raw_js_value = RawJSValue::new(context, value);
462                return Ok(JsValue::Raw(raw_js_value));
463            }
464            let is_array = unsafe { q::JS_IsArray(context, *r) } > 0;
465            if is_array {
466                deserialize_array(context, r)
467            } else {
468                let is_resource = unsafe {
469                    Resource::class_id().id.get() > 0 && q::JS_GetClassID(*r) == Resource::class_id().id.get()
470                };
471                if is_resource {
472                    unsafe {
473                        let cls_id = JS_GetClassID(*value);
474                        let cls_obj = JS_GetOpaque2(context, *value, cls_id) as *mut ResourceObject;
475                        let res = (*cls_obj).data.resource.clone();
476                        return Ok(JsValue::Resource(ResourceValue {
477                            resource: res
478                        }))
479                    }
480                }
481                #[cfg(feature = "chrono")]
482                {
483                    use chrono::offset::TimeZone;
484
485                    let date_constructor = js_date_constructor(context);
486                    let is_date = unsafe { q::JS_IsInstanceOf(context, *r, date_constructor) > 0 };
487
488                    if is_date {
489                        let getter = unsafe {
490                            q::JS_GetPropertyStr(
491                                context,
492                                *r,
493                                std::ffi::CStr::from_bytes_with_nul(b"getTime\0")
494                                    .unwrap()
495                                    .as_ptr(),
496                            )
497                        };
498                        assert_eq!(q::JS_VALUE_GET_TAG(getter), q::JS_TAG_OBJECT);
499
500                        let timestamp_raw =
501                            unsafe { q::JS_Call(context, getter, *r, 0, std::ptr::null_mut()) };
502
503                        unsafe {
504                            q::JS_FreeValue(context, getter);
505                            q::JS_FreeValue(context, date_constructor);
506                        };
507
508                        let res = if q::JS_IsFloat64(timestamp_raw) {
509                            let f = unsafe { q::JS_VALUE_GET_FLOAT64(timestamp_raw) } as i64;
510                            let datetime = chrono::Utc.timestamp_millis(f);
511                            Ok(JsValue::Date(datetime))
512                        } else if q::JS_IsInt(timestamp_raw) {
513                            let f = unsafe { q::JS_VALUE_GET_INT(timestamp_raw) } as i64;
514                            let datetime = chrono::Utc.timestamp_millis(f);
515                            Ok(JsValue::Date(datetime))
516                        } else {
517                            Err(ValueError::Internal(
518                                "Could not convert 'Date' instance to timestamp".into(),
519                            ))
520                        };
521                        return res;
522                    } else {
523                        unsafe { q::JS_FreeValue(context, date_constructor) };
524                    }
525                }
526                let raw_js_value = RawJSValue::new(context, value);
527                return Ok(JsValue::Raw(raw_js_value));
528            }
529        }
530        // BigInt
531        #[cfg(feature = "bigint")]
532        TAG_BIG_INT => {
533            let mut int: i64 = 0;
534            let ret = unsafe { q::JS_ToBigInt64(context, &mut int, *r) };
535            if ret == 0 {
536                Ok(JsValue::BigInt(BigInt {
537                    inner: BigIntOrI64::Int(int),
538                }))
539            } else {
540                let ptr = unsafe { q::JS_ToCStringLen2(context, std::ptr::null_mut(), *r, 0) };
541
542                if ptr.is_null() {
543                    return Err(ValueError::Internal(
544                        "Could not convert BigInt to string: got a null pointer".into(),
545                    ));
546                }
547
548                let cstr = unsafe { std::ffi::CStr::from_ptr(ptr) };
549                let bigint = num_bigint::BigInt::parse_bytes(cstr.to_bytes(), 10).unwrap();
550
551                // Free the c string.
552                unsafe { q::JS_FreeCString(context, ptr) };
553
554                Ok(JsValue::BigInt(BigInt {
555                    inner: BigIntOrI64::BigInt(bigint),
556                }))
557            }
558        }
559        q::JS_TAG_EXCEPTION => {
560            let raw_js_value = RawJSValue::new(context, value);
561            Ok(JsValue::Exception(raw_js_value))
562        }
563        t => {
564            if q::JS_IsFloat64(*value) {
565                Ok(JsValue::Float(unsafe {
566                    q::JS_VALUE_GET_FLOAT64(*value)
567                }))
568            } else {
569                // println!("unknown tag: {}", t);
570                let raw_js_value = RawJSValue::new(context, value);
571                Ok(JsValue::Raw(raw_js_value))
572            }
573        }
574    }
575}