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