qjs 0.1.2

Rust binding for the QuickJS Javascript Engine
Documentation
#![allow(clippy::cast_lossless)]

use std::cmp::Ordering;
use std::ffi::{CStr, CString};
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::os::raw::c_char;
use std::ptr::NonNull;
use std::slice;

use failure::Error;
use foreign_types::ForeignTypeRef;

pub use crate::ffi::_bindgen_ty_1::*;
use crate::{
    ffi,
    handle::{Bindable, Unbindable},
    ClassId, ContextRef, Local, RuntimeRef,
};

pub const ERR: i32 = -1;
pub const TRUE: i32 = 1;
pub const FALSE: i32 = 0;

/// `Value` represents a Javascript value which can be a primitive type or an object.
#[repr(transparent)]
pub struct Value(ffi::JSValue);

impl Unbindable for Value {
    fn unbind(ctxt: &ContextRef, inner: Self) {
        ctxt.free_value(inner)
    }
}

impl fmt::Debug for Value {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        unsafe {
            match self.tag() {
                JS_TAG_BIG_INT => f.debug_tuple("BigInt").field(&self.u.ptr).finish(),
                JS_TAG_BIG_FLOAT => f.debug_tuple("BigFloat").field(&self.u.ptr).finish(),
                JS_TAG_SYMBOL => f.debug_tuple("Symbol").field(&self.u.ptr).finish(),
                JS_TAG_STRING => f.debug_tuple("String").field(&self.u.ptr).finish(),
                JS_TAG_MODULE => f.debug_tuple("Module").field(&self.u.ptr).finish(),
                JS_TAG_FUNCTION_BYTECODE => f.debug_tuple("Function").field(&self.u.ptr).finish(),
                JS_TAG_OBJECT => f.debug_tuple("Object").field(&self.u.ptr).finish(),
                JS_TAG_INT => f.debug_tuple("Value").field(&self.u.int32).finish(),
                JS_TAG_BOOL => f
                    .debug_tuple("Value")
                    .field(&(self.u.int32 != FALSE))
                    .finish(),
                JS_TAG_NULL => f.write_str("Null"),
                JS_TAG_UNDEFINED => f.write_str("Undefined"),
                JS_TAG_UNINITIALIZED => f.write_str("Uninitialized"),
                JS_TAG_CATCH_OFFSET => f.debug_tuple("CatchOffset").field(&self.u.int32).finish(),
                JS_TAG_EXCEPTION => f.write_str("Exception"),
                JS_TAG_FLOAT64 => f.debug_tuple("Value").field(&self.u.float64).finish(),
                tag => f.debug_struct("Value").field("tag", &tag).finish(),
            }
        }
    }
}

impl Deref for Value {
    type Target = ffi::JSValue;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for Value {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl From<ffi::JSValue> for Value {
    fn from(v: ffi::JSValue) -> Self {
        Value(v)
    }
}

impl Into<ffi::JSValue> for Value {
    fn into(self) -> ffi::JSValue {
        self.0
    }
}

impl<'a> Into<Value> for Local<'a, Value> {
    fn into(self) -> Value {
        self.into_inner()
    }
}

impl<'a> Into<ffi::JSValue> for Local<'a, Value> {
    fn into(self) -> ffi::JSValue {
        self.into_inner().raw()
    }
}

impl RuntimeRef {
    pub fn free_value(&self, v: Value) {
        if v.has_ref_cnt() {
            unsafe {
                let mut ref_cnt = v.as_ptr::<ffi::JSRefCountHeader>();

                ref_cnt.as_mut().ref_count -= 1;

                if ref_cnt.as_ref().ref_count <= 0 {
                    ffi::__JS_FreeValueRT(self.as_ptr(), v.0)
                }
            }
        }
    }
}

impl fmt::Display for Local<'_, Value> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(&self.to_cstring().unwrap().to_string_lossy().to_string())
    }
}

impl fmt::Debug for Local<'_, Value> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_tuple("Value").field(&self.to_string()).finish()
    }
}

impl Clone for Local<'_, Value> {
    fn clone(&self) -> Self {
        self.ctxt.clone_value(self)
    }
}

impl<'a> Local<'a, Value> {
    pub fn check_undefined(self) -> Option<Local<'a, Value>> {
        if self.is_undefined() {
            None
        } else {
            Some(self)
        }
    }

    pub fn is_error(&self) -> bool {
        self.ctxt.is_error(self)
    }

    pub fn is_function(&self) -> bool {
        self.ctxt.is_function(self)
    }

    pub fn is_constructor(&self) -> bool {
        self.ctxt.is_constructor(self)
    }

    pub fn to_bool(&self) -> Option<bool> {
        self.ctxt.to_bool(self)
    }

    pub fn to_int32(&self) -> Option<i32> {
        self.ctxt.to_int32(self)
    }

    pub fn to_int64(&self) -> Option<i64> {
        self.ctxt.to_int64(self)
    }

    pub fn to_index(&self) -> Option<u64> {
        self.ctxt.to_index(self)
    }

    pub fn to_float64(&self) -> Option<f64> {
        self.ctxt.to_float64(self)
    }

    #[cfg(feature = "bignum")]
    pub fn to_bigint64(&self) -> Option<i64> {
        self.ctxt.to_bigint64(self)
    }

    pub fn to_str(&self) -> Local<Value> {
        self.ctxt.bind(self.ctxt.to_str(self))
    }

    pub fn to_property_key(&self) -> Local<Value> {
        self.ctxt.bind(self.ctxt.to_property_key(self))
    }

    pub fn to_cstring(&self) -> Option<CString> {
        self.ctxt.to_cstring(self)
    }

    pub fn instance_of(&self, obj: &Value) -> Result<bool, Error> {
        self.ctxt.is_instance_of(self, obj)
    }
}

impl ContextRef {
    pub fn clone_value(&self, v: &Value) -> Local<Value> {
        unsafe {
            if v.has_ref_cnt() {
                v.as_ptr::<ffi::JSRefCountHeader>().as_mut().ref_count += 1;
            }
        }

        self.bind(v.0)
    }

    pub fn free_value<T: Into<Value>>(&self, v: T) {
        let v = v.into();

        if v.has_ref_cnt() {
            unsafe {
                let mut ref_cnt = v.as_ptr::<ffi::JSRefCountHeader>();

                ref_cnt.as_mut().ref_count -= 1;

                if ref_cnt.as_ref().ref_count <= 0 {
                    ffi::__JS_FreeValue(self.as_ptr(), v.0)
                }
            }
        }
    }

    pub fn nan(&self) -> Local<Value> {
        self.bind(Value::nan())
    }

    pub fn null(&self) -> Local<Value> {
        self.bind(Value::null())
    }

    pub fn undefined(&self) -> Local<Value> {
        self.bind(Value::undefined())
    }

    pub fn false_value(&self) -> Local<Value> {
        self.bind(Value::false_value())
    }

    pub fn true_value(&self) -> Local<Value> {
        self.bind(Value::true_value())
    }

    pub fn exception(&self) -> Local<Value> {
        self.bind(Value::exception())
    }

    pub fn uninitialized(&self) -> Local<Value> {
        self.bind(Value::uninitialized())
    }

    pub fn new_value<T: NewValue>(&self, s: T) -> Value {
        Value(s.new_value(self))
    }

    pub fn new_object_proto_class(&self, proto: &Value, class_id: ClassId) -> Value {
        Value(unsafe { ffi::JS_NewObjectProtoClass(self.as_ptr(), proto.raw(), class_id) })
    }

    pub fn new_object_class(&self, class_id: ClassId) -> Value {
        Value(unsafe { ffi::JS_NewObjectClass(self.as_ptr(), class_id as i32) })
    }

    pub fn new_object_proto(&self, proto: &Value) -> Value {
        Value(unsafe { ffi::JS_NewObjectProto(self.as_ptr(), proto.raw()) })
    }

    pub fn new_object(&self) -> Value {
        Value(unsafe { ffi::JS_NewObject(self.as_ptr()) })
    }

    pub fn new_array(&self) -> Value {
        Value(unsafe { ffi::JS_NewArray(self.as_ptr()) })
    }

    pub fn new_catch_offset(&self, off: i32) -> Value {
        Value(mkval(JS_TAG_CATCH_OFFSET, off))
    }

    #[cfg(feature = "bignum")]
    pub fn new_bigint64(&self, n: i64) -> Value {
        Value(unsafe { ffi::JS_NewBigInt64(self.as_ptr(), n) })
    }

    #[cfg(feature = "bignum")]
    pub fn new_biguint64(&self, n: u64) -> Value {
        Value(unsafe { ffi::JS_NewBigUint64(self.as_ptr(), n) })
    }

    pub fn to_bool(&self, val: &Value) -> Option<bool> {
        match unsafe { ffi::JS_ToBool(self.as_ptr(), val.0) } {
            ERR => None,
            FALSE => Some(false),
            _ => Some(true),
        }
    }

    pub fn to_int32(&self, val: &Value) -> Option<i32> {
        let mut n = 0;

        match unsafe { ffi::JS_ToInt32(self.as_ptr(), &mut n, val.0) } {
            ERR => None,
            _ => Some(n),
        }
    }

    pub fn to_int64(&self, val: &Value) -> Option<i64> {
        let mut n = 0;

        match unsafe { ffi::JS_ToInt64(self.as_ptr(), &mut n, val.0) } {
            ERR => None,
            _ => Some(n),
        }
    }

    pub fn to_index(&self, val: &Value) -> Option<u64> {
        let mut n = 0;

        match unsafe { ffi::JS_ToIndex(self.as_ptr(), &mut n, val.0) } {
            ERR => None,
            _ => Some(n),
        }
    }

    pub fn to_float64(&self, val: &Value) -> Option<f64> {
        let mut n = 0.0;

        match unsafe { ffi::JS_ToFloat64(self.as_ptr(), &mut n, val.0) } {
            ERR => None,
            _ => Some(n),
        }
    }

    #[cfg(feature = "bignum")]
    pub fn to_bigint64(&self, val: &Value) -> Option<i64> {
        let mut n = 0;

        match unsafe { ffi::JS_ToBigInt64(self.as_ptr(), &mut n, val.0) } {
            ERR => None,
            _ => Some(n),
        }
    }

    pub fn to_str(&self, val: &Value) -> Value {
        Value(unsafe { ffi::JS_ToString(self.as_ptr(), val.0) })
    }

    pub fn to_property_key(&self, val: &Value) -> Value {
        Value(unsafe { ffi::JS_ToPropertyKey(self.as_ptr(), val.0) })
    }

    /// Convert Javascript String to C UTF-8 encoded strings.
    pub fn to_cstring(&self, val: &Value) -> Option<CString> {
        let mut len = 0;

        unsafe {
            let p = ffi::JS_ToCStringLen2(self.as_ptr(), &mut len, val.0, FALSE);

            if p.is_null() {
                None
            } else {
                let s = CStr::from_bytes_with_nul_unchecked(slice::from_raw_parts(
                    p as *const _,
                    len as usize + 1,
                ))
                .to_owned();

                ffi::JS_FreeCString(self.as_ptr(), p);

                Some(s)
            }
        }
    }

    pub fn is_instance_of(&self, val: &Value, obj: &Value) -> Result<bool, Error> {
        self.check_bool(unsafe { ffi::JS_IsInstanceOf(self.as_ptr(), val.raw(), obj.raw()) })
    }
}

impl<'a, T> Bindable<'a> for T
where
    T: NewValue,
{
    type Output = Value;

    fn bind_to(self, ctxt: &ContextRef) -> Self::Output {
        Value(self.new_value(ctxt))
    }
}

impl From<bool> for Value {
    fn from(v: bool) -> Self {
        Value(mkval(JS_TAG_BOOL, if v { TRUE } else { FALSE }))
    }
}

impl From<i32> for Value {
    fn from(v: i32) -> Self {
        Value(mkval(JS_TAG_INT, v))
    }
}

impl From<f64> for Value {
    fn from(v: f64) -> Self {
        Value(ffi::JSValue {
            u: ffi::JSValueUnion { float64: v },
            tag: JS_TAG_FLOAT64 as i64,
        })
    }
}

/// Create new `Value` from primitive.
pub trait NewValue {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue;
}

impl NewValue for bool {
    fn new_value(self, _ctxt: &ContextRef) -> ffi::JSValue {
        Value::from(self).0
    }
}

impl NewValue for u8 {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        i32::from(self).new_value(ctxt)
    }
}

impl NewValue for u16 {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        i32::from(self).new_value(ctxt)
    }
}

impl NewValue for u32 {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        i64::from(self).new_value(ctxt)
    }
}

impl NewValue for u64 {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        if cfg!(feature = "bignum") {
            unsafe { ffi::JS_NewBigUint64(ctxt.as_ptr(), self) }
        } else {
            (self as i64).new_value(ctxt)
        }
    }
}

impl NewValue for i8 {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        i32::from(self).new_value(ctxt)
    }
}

impl NewValue for i16 {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        i32::from(self).new_value(ctxt)
    }
}

impl NewValue for i32 {
    fn new_value(self, _ctxt: &ContextRef) -> ffi::JSValue {
        Value::from(self).0
    }
}

impl NewValue for i64 {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        if cfg!(feature = "bignum") {
            unsafe { ffi::JS_NewBigInt64(ctxt.as_ptr(), self) }
        } else {
            unsafe { ffi::JS_NewInt64(ctxt.as_ptr(), self) }
        }
    }
}

impl NewValue for f32 {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        f64::from(self).new_value(ctxt)
    }
}

impl NewValue for f64 {
    fn new_value(self, _ctxt: &ContextRef) -> ffi::JSValue {
        Value::from(self).0
    }
}

impl NewValue for String {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        self.as_str().new_value(ctxt)
    }
}

impl<'a> NewValue for &'a str {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        unsafe { ffi::JS_NewStringLen(ctxt.as_ptr(), self.as_ptr() as *const _, self.len()) }
    }
}

impl NewValue for *const c_char {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        unsafe { ffi::JS_NewString(ctxt.as_ptr(), self) }
    }
}

impl NewValue for ffi::JSValue {
    fn new_value(self, _ctxt: &ContextRef) -> ffi::JSValue {
        self
    }
}

impl NewValue for Value {
    fn new_value(self, _ctxt: &ContextRef) -> ffi::JSValue {
        self.raw()
    }
}

impl<'a> NewValue for &'a Value {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        ctxt.clone_value(self).into()
    }
}

impl<'a> NewValue for Local<'a, Value> {
    fn new_value(self, _ctxt: &ContextRef) -> ffi::JSValue {
        self.into_inner().into()
    }
}

impl<'a> NewValue for &'a Local<'a, Value> {
    fn new_value(self, ctxt: &ContextRef) -> ffi::JSValue {
        ctxt.clone_value(self).into()
    }
}

/// Extract primitive from `Local<Value>`.
pub trait ExtractValue: Sized {
    /// Extract primitive from `Local<Value>`.
    fn extract_value(v: &Local<Value>) -> Option<Self>;
}

impl ExtractValue for () {
    fn extract_value(v: &Local<Value>) -> Option<Self> {
        if v.is_null() {
            None
        } else {
            Some(())
        }
    }
}

impl ExtractValue for bool {
    fn extract_value(v: &Local<Value>) -> Option<Self> {
        v.as_bool().or_else(|| v.to_bool())
    }
}

impl ExtractValue for i32 {
    fn extract_value(v: &Local<Value>) -> Option<Self> {
        v.as_int().or_else(|| v.to_int32())
    }
}

impl ExtractValue for i64 {
    fn extract_value(v: &Local<Value>) -> Option<Self> {
        v.as_int().map(i64::from).or_else(|| v.to_int64())
    }
}

impl ExtractValue for u64 {
    fn extract_value(v: &Local<Value>) -> Option<Self> {
        v.to_index()
    }
}

impl ExtractValue for f64 {
    fn extract_value(v: &Local<Value>) -> Option<Self> {
        v.as_float().or_else(|| v.to_float64())
    }
}

impl ExtractValue for String {
    fn extract_value(v: &Local<Value>) -> Option<Self> {
        v.to_cstring().map(|s| s.to_string_lossy().to_string())
    }
}

impl<T: ExtractValue + PartialEq> PartialEq<T> for Local<'_, Value> {
    fn eq(&self, other: &T) -> bool {
        T::extract_value(self).map_or(false, |v| v.eq(other))
    }
}

impl<T: ExtractValue + PartialOrd> PartialOrd<T> for Local<'_, Value> {
    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
        T::extract_value(self).and_then(|v| v.partial_cmp(other))
    }
}

const fn mkval(tag: i32, val: i32) -> ffi::JSValue {
    ffi::JSValue {
        tag: tag as i64,
        u: ffi::JSValueUnion { int32: val },
    }
}

#[allow(dead_code)]
const fn mkptr<T>(tag: i32, val: *mut T) -> ffi::JSValue {
    ffi::JSValue {
        tag: tag as i64,
        u: ffi::JSValueUnion { ptr: val as *mut _ },
    }
}

impl Value {
    pub fn new(value: ffi::JSValue) -> Option<Self> {
        let value = Value(value);

        if value.is_undefined() {
            None
        } else {
            Some(value)
        }
    }

    pub const fn nan() -> Self {
        Value(ffi::JSValue {
            u: ffi::JSValueUnion {
                float64: std::f64::NAN,
            },
            tag: JS_TAG_FLOAT64 as i64,
        })
    }

    pub const fn null() -> Self {
        Value(mkval(JS_TAG_NULL, 0))
    }

    pub const fn undefined() -> Self {
        Value(mkval(JS_TAG_UNDEFINED, 0))
    }

    pub const fn false_value() -> Self {
        Value(mkval(JS_TAG_BOOL, FALSE))
    }

    pub const fn true_value() -> Self {
        Value(mkval(JS_TAG_BOOL, TRUE))
    }

    pub const fn exception() -> Self {
        Value(mkval(JS_TAG_EXCEPTION, 0))
    }

    pub const fn uninitialized() -> Self {
        Value(mkval(JS_TAG_UNINITIALIZED, 0))
    }

    pub fn raw(&self) -> ffi::JSValue {
        self.0
    }

    pub fn tag(&self) -> i32 {
        self.tag as i32
    }

    pub fn is_number(&self) -> bool {
        unsafe { ffi::JS_IsNumber(self.raw()) != FALSE }
    }

    pub fn is_integer(&self) -> bool {
        let tag = self.tag();

        tag == JS_TAG_INT || tag == JS_TAG_BIG_INT
    }

    pub fn is_big_float(&self) -> bool {
        self.tag() == JS_TAG_BIG_FLOAT
    }

    pub fn is_bool(&self) -> bool {
        self.tag() == JS_TAG_BOOL
    }

    pub fn is_null(&self) -> bool {
        self.tag() == JS_TAG_NULL
    }

    pub fn is_undefined(&self) -> bool {
        self.tag() == JS_TAG_UNDEFINED
    }

    pub fn is_exception(&self) -> bool {
        self.tag() == JS_TAG_EXCEPTION
    }

    pub fn is_uninitialized(&self) -> bool {
        self.tag() == JS_TAG_UNINITIALIZED
    }

    pub fn is_string(&self) -> bool {
        self.tag() == JS_TAG_STRING
    }

    pub fn is_symbol(&self) -> bool {
        self.tag() == JS_TAG_SYMBOL
    }

    pub fn is_object(&self) -> bool {
        self.tag() == JS_TAG_OBJECT
    }

    pub fn as_int(&self) -> Option<i32> {
        if self.tag() == JS_TAG_INT {
            Some(unsafe { self.u.int32 })
        } else {
            None
        }
    }

    pub fn as_bool(&self) -> Option<bool> {
        if self.tag() == JS_TAG_BOOL {
            Some(unsafe { self.u.int32 != 0 })
        } else {
            None
        }
    }

    pub fn as_float(&self) -> Option<f64> {
        if self.tag() == JS_TAG_FLOAT64 {
            Some(unsafe { self.u.float64 })
        } else {
            None
        }
    }

    pub fn as_object(&self) -> Option<NonNull<ffi::JSObject>> {
        if self.tag() == JS_TAG_OBJECT {
            Some(self.as_ptr())
        } else {
            None
        }
    }

    pub fn check_undefined(&self) -> Option<&Value> {
        if self.is_undefined() {
            None
        } else {
            Some(self)
        }
    }

    pub fn as_ptr<T>(&self) -> NonNull<T> {
        unsafe { NonNull::new_unchecked(self.u.ptr).cast() }
    }

    pub fn ref_cnt(&self) -> Option<i32> {
        if self.has_ref_cnt() {
            Some(unsafe { self.as_ptr::<ffi::JSRefCountHeader>().as_ref().ref_count })
        } else {
            None
        }
    }

    fn has_ref_cnt(&self) -> bool {
        (self.tag() as u32) >= (JS_TAG_FIRST as u32)
    }
}

#[cfg(test)]
mod tests {
    use crate::{Context, Eval, Runtime};

    #[test]
    fn instance_of() {
        let _ = pretty_env_logger::try_init();

        let rt = Runtime::new();
        let ctxt = Context::new(&rt);

        let car = ctxt
            .eval_script(
                r#"
function Car(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
}

function Person(name, age) {
    this.name = name;
    this.age = age;
}

new Car('Honda', 'Accord', 1998)"#,
                "<evalScript>",
                Eval::GLOBAL,
            )
            .unwrap();

        let global = ctxt.global_object();

        assert!(car
            .instance_of(&global.get_property("Car").unwrap())
            .unwrap());
        assert!(!car
            .instance_of(&global.get_property("Person").unwrap())
            .unwrap());
    }
}