diff --git a/Cargo.toml b/Cargo.toml index 5ba2515..2a5b457 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,23 +11,30 @@ authors = ["Christoph Herzog "] keywords = ["quickjs", "javascript", "js", "engine", "interpreter"] [package.metadata.docs.rs] -features = [ "chrono", "bigint", "log" ] +features = ["chrono", "bigint", "log"] [features] default = ["chrono"] patched = ["libquickjs-sys/patched"] bigint = ["num-bigint", "num-traits", "libquickjs-sys/patched"] +serde = ["dep:quick-js-serde", "dep:serde"] [dependencies] libquickjs-sys = { version = ">= 0.9.0, < 0.10.0", path = "./libquickjs-sys" } +quick-js-serde = { path = "./serde", optional = true } +serde = { version = "1", optional = true } chrono = { version = "0.4.7", optional = true } num-bigint = { version = "0.2.2", optional = true } num-traits = { version = "0.2.0", optional = true } log = { version = "0.4.8", optional = true } once_cell = "1.2.0" +[dev-dependencies] +serde = { version = "1.0.176", features = ['derive'] } + [workspace] members = [ "libquickjs-sys", + "serde" ] diff --git a/examples/serde.rs b/examples/serde.rs new file mode 100644 index 0000000..4483836 --- /dev/null +++ b/examples/serde.rs @@ -0,0 +1,41 @@ +use quick_js::Context; +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct Inner { + b: u8, +} + +#[derive(Debug, Serialize)] +pub struct Example { + a: Vec, +} + +fn main() { + let context = Context::new().unwrap(); + + let value = context.eval("1 + 2").unwrap(); + println!("js: 1 + 2 = {:?}", value); + + context + .add_callback("myCallback", |a: i32, b: i32| a + b * b) + .unwrap(); + + context + .set_global_serde( + "example", + &Example { + a: vec![Inner { b: 5 }, Inner { b: 6 }], + }, + ) + .unwrap(); + + let value = context + .eval( + r#" + JSON.stringify(example) +"#, + ) + .unwrap(); + println!("js: JSON.stringify(example) = {:?}", value); +} diff --git a/libquickjs-sys/src/static-functions.rs b/libquickjs-sys/src/static-functions.rs index 751f4be..66c5f85 100644 --- a/libquickjs-sys/src/static-functions.rs +++ b/libquickjs-sys/src/static-functions.rs @@ -1,4 +1,3 @@ - extern "C" { fn JS_ValueGetTag_real(v: JSValue) -> i32; fn JS_DupValue_real(ctx: *mut JSContext, v: JSValue); @@ -24,9 +23,26 @@ extern "C" { fn JS_IsSymbol_real(v: JSValue) -> bool; fn JS_IsObject_real(v: JSValue) -> bool; fn JS_ToUint32_real(ctx: *mut JSContext, pres: u32, val: JSValue) -> u32; - fn JS_SetProperty_real(ctx: *mut JSContext, this_obj: JSValue, prop: JSAtom, val: JSValue) -> ::std::os::raw::c_int; - fn JS_NewCFunction_real(ctx: *mut JSContext, func: *mut JSCFunction, name: *const ::std::os::raw::c_char,length: ::std::os::raw::c_int) -> JSValue; - fn JS_NewCFunctionMagic_real(ctx: *mut JSContext, func: *mut JSCFunctionMagic, name: *const ::std::os::raw::c_char, length: ::std::os::raw::c_int, cproto: JSCFunctionEnum, magic: ::std::os::raw::c_int) -> JSValue; + fn JS_SetProperty_real( + ctx: *mut JSContext, + this_obj: JSValue, + prop: JSAtom, + val: JSValue, + ) -> ::std::os::raw::c_int; + fn JS_NewCFunction_real( + ctx: *mut JSContext, + func: *mut JSCFunction, + name: *const ::std::os::raw::c_char, + length: ::std::os::raw::c_int, + ) -> JSValue; + fn JS_NewCFunctionMagic_real( + ctx: *mut JSContext, + func: *mut JSCFunctionMagic, + name: *const ::std::os::raw::c_char, + length: ::std::os::raw::c_int, + cproto: JSCFunctionEnum, + magic: ::std::os::raw::c_int, + ) -> JSValue; } pub unsafe fn JS_ValueGetTag(v: JSValue) -> i32 { @@ -63,7 +79,8 @@ pub unsafe fn JS_NewInt32(ctx: *mut JSContext, v: i32) -> JSValue { JS_NewInt32_real(ctx, v) } -/// create a new f64 value, please note that if the passed f64 fits in a i32 this will return a value with flag 0 (i32) +/// create a new f64 value, please note that if the passed f64 fits in a i32 +/// this will return a value with flag 0 (i32) pub unsafe fn JS_NewFloat64(ctx: *mut JSContext, v: f64) -> JSValue { JS_NewFloat64_real(ctx, v) } @@ -90,7 +107,7 @@ pub unsafe fn JS_IsNumber(v: JSValue) -> bool { /// check if a JSValue is a BigInt pub unsafe fn JS_IsBigInt(ctx: *mut JSContext, v: JSValue) -> bool { - JS_IsBigInt_real(ctx,v) + JS_IsBigInt_real(ctx, v) } /// check if a JSValue is a BigFloat @@ -119,7 +136,7 @@ pub unsafe fn JS_IsUndefined(v: JSValue) -> bool { } /// check if a JSValue is an Exception -pub unsafe fn JS_IsException(v: JSValue) -> bool{ +pub unsafe fn JS_IsException(v: JSValue) -> bool { JS_IsException_real(v) } @@ -149,16 +166,33 @@ pub unsafe fn JS_ToUint32(ctx: *mut JSContext, pres: u32, val: JSValue) -> u32 { } /// set a property of an object identified by a JSAtom -pub unsafe fn JS_SetProperty(ctx: *mut JSContext, this_obj: JSValue, prop: JSAtom, val: JSValue) -> ::std::os::raw::c_int { +pub unsafe fn JS_SetProperty( + ctx: *mut JSContext, + this_obj: JSValue, + prop: JSAtom, + val: JSValue, +) -> ::std::os::raw::c_int { JS_SetProperty_real(ctx, this_obj, prop, val) } /// create a new Function based on a JSCFunction -pub unsafe fn JS_NewCFunction(ctx: *mut JSContext, func: *mut JSCFunction, name: *const ::std::os::raw::c_char,length: ::std::os::raw::c_int) -> JSValue { +pub unsafe fn JS_NewCFunction( + ctx: *mut JSContext, + func: *mut JSCFunction, + name: *const ::std::os::raw::c_char, + length: ::std::os::raw::c_int, +) -> JSValue { JS_NewCFunction_real(ctx, func, name, length) } /// create a new Function based on a JSCFunction -pub unsafe fn JS_NewCFunctionMagic(ctx: *mut JSContext, func: *mut JSCFunctionMagic, name: *const ::std::os::raw::c_char, length: ::std::os::raw::c_int, cproto: JSCFunctionEnum, magic: ::std::os::raw::c_int) -> JSValue { +pub unsafe fn JS_NewCFunctionMagic( + ctx: *mut JSContext, + func: *mut JSCFunctionMagic, + name: *const ::std::os::raw::c_char, + length: ::std::os::raw::c_int, + cproto: JSCFunctionEnum, + magic: ::std::os::raw::c_int, +) -> JSValue { JS_NewCFunctionMagic_real(ctx, func, name, length, cproto, magic) } diff --git a/serde/Cargo.toml b/serde/Cargo.toml new file mode 100644 index 0000000..90da906 --- /dev/null +++ b/serde/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "quick-js-serde" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libquickjs-sys = { path = "../libquickjs-sys" } + +serde = "1" +thiserror = "1" + +[dev-dependencies] +quick-js = { path = "..", features = ["serde"] } +serde = { version = "1", features = ["derive"] } diff --git a/serde/src/context.rs b/serde/src/context.rs new file mode 100644 index 0000000..3648541 --- /dev/null +++ b/serde/src/context.rs @@ -0,0 +1,34 @@ +use libquickjs_sys::JSContext; + +pub struct Context { + context: *const JSContext, + should_drop: bool, +} + +impl Context { + pub fn new(context: *mut JSContext) -> Self { + Self { + context, + should_drop: true, + } + } + + pub fn new_without_drop(context: *mut JSContext) -> Self { + Self { + context, + should_drop: false, + } + } + + pub(crate) fn as_mut_ptr(&mut self) -> *mut JSContext { + self.context as *mut _ + } +} + +impl Drop for Context { + fn drop(&mut self) { + if self.should_drop { + unsafe { libquickjs_sys::JS_FreeContext(self.context as *mut _) }; + } + } +} diff --git a/serde/src/errors.rs b/serde/src/errors.rs new file mode 100644 index 0000000..138ecd3 --- /dev/null +++ b/serde/src/errors.rs @@ -0,0 +1,119 @@ +use std::ffi::CStr; +use std::str::Utf8Error; + +use libquickjs_sys::{ + JSContext, JSValue, JS_FreeCString, JS_FreeValue, JS_GetException, JS_IsException, JS_IsNull, + JS_IsString, JS_ToCStringLen2, JS_ToString, +}; +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +pub enum Internal { + #[error("Unexpected null pointer")] + UnexpectedNullPointer, + #[error("Unexpected null value")] + UnexpectedNullValue, + #[error("Expected string")] + ExpectedString, + #[error("Invalid UTF-8")] + InvalidUtf8(#[from] Utf8Error), + #[error("Nul byte found in string")] + NulError(#[from] std::ffi::NulError), +} + +unsafe fn get_string(context: *mut JSContext, value: JSValue) -> Result { + if !JS_IsString(value) { + return Err(Internal::ExpectedString); + } + + // convert to a rust string + let ptr = JS_ToCStringLen2(context, std::ptr::null_mut(), value, 0); + + if ptr.is_null() { + return Err(Internal::UnexpectedNullPointer); + } + + let c_str = CStr::from_ptr(ptr); + + let string = c_str.to_str()?.to_string(); + + // Free the C string + JS_FreeCString(context, ptr); + + Ok(string) +} + +unsafe fn exception_to_string( + context: *mut JSContext, + exception: JSValue, +) -> Result { + if JS_IsNull(exception) { + return Err(Internal::UnexpectedNullValue); + } + + let exception = if JS_IsString(exception) { + exception + } else { + JS_ToString(context, exception) + }; + + get_string(context, exception) +} + +#[derive(Debug, Clone, Error)] +pub enum SerializationError { + #[error("Out of memory")] + OutOfMemory, + #[error("Internal error: {0}")] + Internal(#[from] Internal), + #[error("Unknown error: {0}")] + Unknown(String), + #[error("Expected call to `serialize_key` before `serialize_value`")] + MissingKey, + #[error("Expected call times of calls to `serialize_key` and `serialize_value` to be equal")] + MissingValue, + #[error("Expected either a string or a number as a key")] + InvalidKey, + #[error("The serializer is in an invalid state")] + InvalidState, + #[error("The number is too large to be represented")] + IntTooLarge, +} + +impl SerializationError { + pub fn from_exception(context: *mut JSContext) -> Self { + // https://bellard.org/quickjs/quickjs.html#Exceptions 3.4.4 + let exception = unsafe { JS_GetException(context) }; + + let value = unsafe { exception_to_string(context, exception) }; + + match value { + Ok(value) => { + if value.contains("out of memory") { + Self::OutOfMemory + } else { + Self::Unknown(value) + } + } + Err(err) => err.into(), + } + } + + pub fn try_from_value(context: *mut JSContext, value: JSValue) -> Result { + if unsafe { JS_IsException(value) } { + // we're for sure an error, we just don't know which one + // TODO: do we need to free here? + unsafe { JS_FreeValue(context, value) } + + Err(Self::from_exception(context)) + } else { + Ok(value) + } + } +} + +impl serde::ser::Error for SerializationError { + fn custom(msg: T) -> Self { + Self::Unknown(msg.to_string()) + } +} diff --git a/serde/src/lib.rs b/serde/src/lib.rs new file mode 100644 index 0000000..d73b503 --- /dev/null +++ b/serde/src/lib.rs @@ -0,0 +1,19 @@ +mod context; +mod errors; +mod ser; + +pub use context::Context; +use libquickjs_sys::JSValue; +use serde::Serialize; + +pub fn serialize( + value: &T, + context: &mut Context, +) -> Result +where + T: Serialize, +{ + let serializer = ser::Serializer::new(context); + + value.serialize(serializer) +} diff --git a/serde/src/ser/map.rs b/serde/src/ser/map.rs new file mode 100644 index 0000000..17275d5 --- /dev/null +++ b/serde/src/ser/map.rs @@ -0,0 +1,181 @@ +use libquickjs_sys::{ + JSAtom, JSValue, JS_FreeAtom, JS_FreeValue, JS_NewObject, JS_SetProperty, JS_ValueToAtom, + JS_ATOM_NULL, +}; +use serde::Serialize; + +use crate::context::Context; +use crate::errors::SerializationError; +use crate::ser::Serializer; + +pub struct SerializeMap<'a> { + pub(crate) context: &'a mut Context, + object: Option, + + pending_key: Option, +} + +impl<'a> SerializeMap<'a> { + pub(crate) fn new(context: &'a mut Context) -> Result { + let object = unsafe { JS_NewObject(context.as_mut_ptr()) }; + let object = SerializationError::try_from_value(context.as_mut_ptr(), object)?; + + Ok(Self { + context, + object: Some(object), + + pending_key: None, + }) + } + + fn key_to_atom(&mut self, key: JSValue) -> Result { + let atom = unsafe { JS_ValueToAtom(self.context.as_mut_ptr(), key) }; + + // free the key value + unsafe { JS_FreeValue(self.context.as_mut_ptr(), key) }; + + if atom == JS_ATOM_NULL { + return Err(SerializationError::InvalidKey); + } + + Ok(atom) + } + + fn insert(&mut self, key: JSValue, value: JSValue) -> Result<(), SerializationError> { + // IMPORTANT: This is on top, so that we don't need to free the value in case of + // an error. + let object = self.object.ok_or(SerializationError::InvalidState)?; + + let key = self.key_to_atom(key)?; + + let error = unsafe { JS_SetProperty(self.context.as_mut_ptr(), object, key, value) }; + + if error == -1 { + // exception occurred, time to roll back + let error = SerializationError::from_exception(self.context.as_mut_ptr()); + + // free the value and key + unsafe { JS_FreeValue(self.context.as_mut_ptr(), value) }; + unsafe { JS_FreeAtom(self.context.as_mut_ptr(), key) }; + + return Err(error); + } + + // The value is freed by JS_SetProperty, the key is not freed + unsafe { JS_FreeAtom(self.context.as_mut_ptr(), key) }; + + Ok(()) + } + + pub(crate) fn finish_object(&mut self) -> Result { + if self.pending_key.is_some() { + return Err(SerializationError::MissingValue); + } + + self.object.take().ok_or(SerializationError::InvalidState) + } +} + +impl<'a> serde::ser::SerializeMap for SerializeMap<'a> { + type Error = SerializationError; + type Ok = JSValue; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + let serializer = Serializer::new(self.context); + let value = key.serialize(serializer)?; + + self.pending_key = Some(value); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + let key = self + .pending_key + .take() + .ok_or(SerializationError::MissingKey)?; + + let serializer = Serializer::new(self.context); + let value = value.serialize(serializer)?; + + self.insert(key, value)?; + Ok(()) + } + + fn serialize_entry( + &mut self, + key: &K, + value: &V, + ) -> Result<(), Self::Error> + where + K: Serialize, + V: Serialize, + { + // we don't need to buffer the key, we can serialize it directly + + let key = { + let serializer = Serializer::new(self.context); + key.serialize(serializer)? + }; + + let value = { + let serializer = Serializer::new(self.context); + value.serialize(serializer) + }; + + let value = match value { + Ok(value) => value, + Err(error) => { + // free the key + unsafe { JS_FreeValue(self.context.as_mut_ptr(), key) }; + + return Err(error); + } + }; + + self.insert(key, value) + } + + fn end(mut self) -> Result { + self.finish_object() + } +} + +impl<'a> serde::ser::SerializeStruct for SerializeMap<'a> { + type Error = SerializationError; + type Ok = JSValue; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + ::serialize_entry(self, key, value) + } + + fn end(self) -> Result { + ::end(self) + } +} + +impl Drop for SerializeMap<'_> { + fn drop(&mut self) { + // free the object + if let Some(object) = self.object.take() { + unsafe { JS_FreeValue(self.context.as_mut_ptr(), object) }; + } + + // free the pending key + if let Some(key) = self.pending_key.take() { + unsafe { JS_FreeValue(self.context.as_mut_ptr(), key) }; + } + } +} diff --git a/serde/src/ser/mod.rs b/serde/src/ser/mod.rs new file mode 100644 index 0000000..f0697bc --- /dev/null +++ b/serde/src/ser/mod.rs @@ -0,0 +1,274 @@ +mod map; +mod seq; +mod variant; + +use std::ffi::CString; + +use libquickjs_sys::{ + size_t, JSValue, JSValueUnion, JS_NewArrayBufferCopy, JS_NewBigInt64, JS_NewBigUint64, + JS_NewBool, JS_NewFloat64, JS_NewInt32, JS_NewStringLen, JS_TAG_NULL, +}; +use serde::ser::SerializeMap as _; +use serde::Serialize; + +use crate::context::Context; +use crate::errors::{Internal, SerializationError}; +use crate::ser::map::SerializeMap; +use crate::ser::seq::SerializeSeq; +use crate::ser::variant::{SerializeStructVariant, SerializeTupleVariant}; + +pub struct Serializer<'a> { + context: &'a mut Context, +} + +impl<'a> Serializer<'a> { + pub fn new(context: &'a mut Context) -> Self { + Self { context } + } +} + +impl<'a> serde::Serializer for Serializer<'a> { + type Error = SerializationError; + type Ok = JSValue; + type SerializeMap = SerializeMap<'a>; + type SerializeSeq = SerializeSeq<'a>; + type SerializeStruct = SerializeMap<'a>; + type SerializeStructVariant = SerializeStructVariant<'a>; + type SerializeTuple = SerializeSeq<'a>; + type SerializeTupleStruct = SerializeSeq<'a>; + type SerializeTupleVariant = SerializeTupleVariant<'a>; + + fn serialize_bool(self, value: bool) -> Result { + let value = unsafe { JS_NewBool(self.context.as_mut_ptr(), value) }; + SerializationError::try_from_value(self.context.as_mut_ptr(), value) + } + + fn serialize_i8(self, value: i8) -> Result { + self.serialize_i32(i32::from(value)) + } + + fn serialize_i16(self, value: i16) -> Result { + self.serialize_i32(i32::from(value)) + } + + fn serialize_i32(self, value: i32) -> Result { + let value = unsafe { JS_NewInt32(self.context.as_mut_ptr(), value) }; + SerializationError::try_from_value(self.context.as_mut_ptr(), value) + } + + fn serialize_i64(self, value: i64) -> Result { + // try to fit the value into a 32-bit integer, otherwise return a BigInt + if let Ok(value) = i32::try_from(value) { + return self.serialize_i32(value); + } + + let value = unsafe { JS_NewBigInt64(self.context.as_mut_ptr(), value) }; + SerializationError::try_from_value(self.context.as_mut_ptr(), value) + } + + fn serialize_i128(self, value: i128) -> Result { + if let Ok(value) = i64::try_from(value) { + return self.serialize_i64(value); + } + + return Err(SerializationError::IntTooLarge); + } + + // For now we don't support i128 and u128, as there are no methods to create + // BigInts for them. + // In theory we could create our own function to do so, but for now that's + // overkill. + + fn serialize_u8(self, value: u8) -> Result { + self.serialize_i32(i32::from(value)) + } + + fn serialize_u16(self, value: u16) -> Result { + self.serialize_i32(i32::from(value)) + } + + fn serialize_u32(self, value: u32) -> Result { + // we cannot use `JS_NewInt32` here, as there are values in u32 that cannot be + // represented in i32 (and would wrap around) + self.serialize_u64(u64::from(value)) + } + + fn serialize_u64(self, value: u64) -> Result { + // try to fit the value into a 32-bit integer, otherwise return a BigInt + // we could also call `serialize_u64` instead, but that is largely redundant. + if let Ok(value) = i32::try_from(value) { + return self.serialize_i32(value); + } + + let value = unsafe { JS_NewBigUint64(self.context.as_mut_ptr(), value) }; + SerializationError::try_from_value(self.context.as_mut_ptr(), value) + } + + fn serialize_u128(self, value: u128) -> Result { + if let Ok(value) = u64::try_from(value) { + return self.serialize_u64(value); + } + + Err(SerializationError::IntTooLarge) + } + + fn serialize_f32(self, value: f32) -> Result { + self.serialize_f64(f64::from(value)) + } + + fn serialize_f64(self, value: f64) -> Result { + let value = unsafe { JS_NewFloat64(self.context.as_mut_ptr(), value) }; + SerializationError::try_from_value(self.context.as_mut_ptr(), value) + } + + fn serialize_char(self, value: char) -> Result { + let mut buffer = [0; 4]; + let string = value.encode_utf8(&mut buffer); + + self.serialize_str(string) + } + + fn serialize_str(self, value: &str) -> Result { + let c_str = CString::new(value).map_err(Internal::from)?; + + let value = unsafe { + JS_NewStringLen( + self.context.as_mut_ptr(), + c_str.as_ptr(), + value.len() as size_t, + ) + }; + SerializationError::try_from_value(self.context.as_mut_ptr(), value) + } + + fn serialize_bytes(self, value: &[u8]) -> Result { + // TODO: in theory we could also use `JS_NewArrayBuffer` here, but that would be + // _a lot_ more complicated. + let length = value.len(); + + let value = unsafe { + JS_NewArrayBufferCopy(self.context.as_mut_ptr(), value.as_ptr(), length as size_t) + }; + SerializationError::try_from_value(self.context.as_mut_ptr(), value) + } + + fn serialize_none(self) -> Result { + self.serialize_unit() + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + // Unit corresponds to `null` in JS + + // Taken from: https://docs.rs/quickjs_runtime/latest/src/quickjs_runtime/quickjs_utils/mod.rs.html#46-51 + let null = JSValue { + u: JSValueUnion { int32: 0 }, + tag: i64::from(JS_TAG_NULL), + }; + + Ok(null) + } + + fn serialize_unit_struct(self, _: &'static str) -> Result { + self.serialize_unit() + } + + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + ) -> Result { + // We follow the same approach as serde_json here, and serialize the variant as + // a string. + self.serialize_str(variant) + } + + fn serialize_newtype_struct( + self, + _: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + T::serialize(value, self) + } + + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + // We follow the same approach as serde_json here, and serialize the variant as, + // we serialize the value as an object with a single field. + // { `variant`: `value` } + + let mut serializer = self.serialize_map(Some(1))?; + serializer.serialize_entry(variant, value)?; + serializer.end() + } + + fn serialize_seq(self, _: Option) -> Result { + SerializeSeq::new(self.context) + } + + fn serialize_tuple(self, _: usize) -> Result { + SerializeSeq::new(self.context) + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + SerializeSeq::new(self.context) + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + SerializeTupleVariant::new(variant, self.context) + } + + fn serialize_map(self, _: Option) -> Result { + SerializeMap::new(self.context) + } + + fn serialize_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + SerializeMap::new(self.context) + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + SerializeStructVariant::new(variant, self.context) + } + + fn is_human_readable(&self) -> bool { + true + } +} diff --git a/serde/src/ser/seq.rs b/serde/src/ser/seq.rs new file mode 100644 index 0000000..9c4b3bc --- /dev/null +++ b/serde/src/ser/seq.rs @@ -0,0 +1,110 @@ +use libquickjs_sys::{JSValue, JS_FreeValue, JS_NewArray, JS_SetPropertyUint32}; +use serde::Serialize; + +use crate::context::Context; +use crate::errors::SerializationError; +use crate::ser::Serializer; + +pub struct SerializeSeq<'a> { + pub(crate) context: &'a mut Context, + + count: u32, + array: Option, +} + +impl<'a> SerializeSeq<'a> { + pub fn new(context: &'a mut Context) -> Result { + let array = unsafe { JS_NewArray(context.as_mut_ptr()) }; + let array = SerializationError::try_from_value(context.as_mut_ptr(), array) + .expect("failed to create array"); + + Ok(Self { + context, + count: 0, + array: Some(array), + }) + } + + pub(crate) fn finish_array(&mut self) -> Result { + self.array.take().ok_or(SerializationError::InvalidState) + } +} + +impl<'a> serde::ser::SerializeSeq for SerializeSeq<'a> { + type Error = SerializationError; + type Ok = JSValue; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + // IMPORTANT: This is on top, so that we don't need to free the value in case of + // an error. + let array = self.array.ok_or(SerializationError::InvalidState)?; + + let value = { + let serializer = Serializer::new(self.context); + value.serialize(serializer)? + }; + + let error = + unsafe { JS_SetPropertyUint32(self.context.as_mut_ptr(), array, self.count, value) }; + + if error == -1 { + // exception occurred, time to roll back + let error = SerializationError::from_exception(self.context.as_mut_ptr()); + + // free the value + unsafe { JS_FreeValue(self.context.as_mut_ptr(), value) }; + + return Err(error); + } + + self.count += 1; + Ok(()) + } + + fn end(mut self) -> Result { + self.finish_array() + } +} + +impl<'a> serde::ser::SerializeTuple for SerializeSeq<'a> { + type Error = SerializationError; + type Ok = JSValue; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + ::serialize_element(self, value) + } + + fn end(self) -> Result { + ::end(self) + } +} + +impl<'a> serde::ser::SerializeTupleStruct for SerializeSeq<'a> { + type Error = SerializationError; + type Ok = JSValue; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + ::serialize_element(self, value) + } + + fn end(self) -> Result { + ::end(self) + } +} + +impl Drop for SerializeSeq<'_> { + fn drop(&mut self) { + if let Some(array) = self.array.take() { + unsafe { JS_FreeValue(self.context.as_mut_ptr(), array) }; + } + } +} diff --git a/serde/src/ser/variant.rs b/serde/src/ser/variant.rs new file mode 100644 index 0000000..5a552f8 --- /dev/null +++ b/serde/src/ser/variant.rs @@ -0,0 +1,121 @@ +use std::ffi::CString; + +use libquickjs_sys::{JSContext, JSValue, JS_FreeValue, JS_NewObject, JS_SetPropertyStr}; +use serde::Serialize; + +use crate::context::Context; +use crate::errors::{Internal, SerializationError}; +use crate::ser::map::SerializeMap; +use crate::ser::seq::SerializeSeq; + +/// Serialize an enum variant. +/// +/// Serializes an enum variant as `{variant: value}`. +fn finish( + context: *mut JSContext, + variant: &'static str, + value: JSValue, +) -> Result { + // IMPORTANT: we do this conversion before we call finish_object, that way if it + // fails we don't have to worry about freeing the object + // The only one we need to worry about is the given value. + let variant = CString::new(variant).map_err(Internal::from); + + let variant = match variant { + Ok(variant) => variant, + Err(error) => { + // ensure that we don't memory leak + unsafe { JS_FreeValue(context, value) }; + return Err(SerializationError::Internal(error)); + } + }; + + let object = unsafe { JS_NewObject(context) }; + // TODO: check in other places as well + let object = SerializationError::try_from_value(context, object)?; + + let result = unsafe { JS_SetPropertyStr(context, object, variant.as_ptr(), value) }; + + if result < 0 { + unsafe { JS_FreeValue(context, object) }; + unsafe { JS_FreeValue(context, value) }; + + return Err(SerializationError::from_exception(context)); + } + + Ok(object) +} + +pub struct SerializeStructVariant<'a> { + variant: &'static str, + + inner: SerializeMap<'a>, +} + +impl<'a> SerializeStructVariant<'a> { + pub fn new( + variant: &'static str, + context: &'a mut Context, + ) -> Result { + let inner = SerializeMap::new(context)?; + + Ok(Self { variant, inner }) + } +} + +impl<'a> serde::ser::SerializeStructVariant for SerializeStructVariant<'a> { + type Error = SerializationError; + type Ok = JSValue; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + as serde::ser::SerializeMap>::serialize_entry(&mut self.inner, key, value) + } + + fn end(mut self) -> Result { + let inner = self.inner.finish_object()?; + + finish(self.inner.context.as_mut_ptr(), self.variant, inner) + } +} + +pub struct SerializeTupleVariant<'a> { + variant: &'static str, + + inner: SerializeSeq<'a>, +} + +impl<'a> SerializeTupleVariant<'a> { + pub fn new( + variant: &'static str, + context: &'a mut Context, + ) -> Result { + let inner = SerializeSeq::new(context)?; + + Ok(Self { variant, inner }) + } +} + +impl<'a> serde::ser::SerializeTupleVariant for SerializeTupleVariant<'a> { + type Error = SerializationError; + type Ok = JSValue; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + as serde::ser::SerializeSeq>::serialize_element(&mut self.inner, value) + } + + fn end(mut self) -> Result { + let inner = self.inner.finish_array()?; + + finish(self.inner.context.as_mut_ptr(), self.variant, inner) + } +} diff --git a/serde/tests/ser.rs b/serde/tests/ser.rs new file mode 100644 index 0000000..983a5f2 --- /dev/null +++ b/serde/tests/ser.rs @@ -0,0 +1,478 @@ +use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use quick_js::{Context, ExecutionError}; +use serde::ser::{Error, SerializeMap}; +use serde::{Serialize, Serializer}; + +fn run_serialize_error(value: &T) -> ExecutionError +where + T: Serialize, +{ + let context = Context::new().unwrap(); + + let result = context.set_global_serde("example", value); + + result.expect_err("serialization should fail") +} + +fn run(value: &T) -> String +where + T: Serialize, +{ + let context = Context::new().unwrap(); + + context.set_global_serde("example", value).unwrap(); + + context + .eval_as::("JSON.stringify(example)") + .unwrap() +} + +#[test] +fn u8() { + assert_eq!(run(&5u8), "5"); +} + +#[test] +fn u16() { + assert_eq!(run(&5u16), "5"); +} + +#[test] +fn u32() { + assert_eq!(run(&5u32), "5"); +} + +#[test] +fn u64() { + assert_eq!(run(&5u64), "5"); +} + +#[test] +fn u128() { + assert_eq!(run(&5u128), "5"); +} + +#[test] +fn i8() { + assert_eq!(run(&-5i8), "-5"); +} + +#[test] +fn i16() { + assert_eq!(run(&-5i16), "-5"); +} + +#[test] +fn i32() { + assert_eq!(run(&-5i32), "-5"); +} + +#[test] +fn i64() { + assert_eq!(run(&-5i64), "-5"); +} + +#[test] +fn i128() { + assert_eq!(run(&-5i128), "-5"); +} + +#[test] +fn bool() { + assert_eq!(run(&true), "true"); + assert_eq!(run(&false), "false"); +} + +#[test] +fn char() { + assert_eq!(run(&'a'), r#""a""#); +} + +#[test] +fn str() { + assert_eq!(run(&"abc"), r#""abc""#); +} + +#[test] +fn string() { + assert_eq!(run(&String::from("abc")), r#""abc""#); +} + +#[test] +fn unit() { + assert_eq!(run(&()), "null"); +} + +#[test] +fn option() { + assert_eq!(run(&Some(5u8)), "5"); + assert_eq!(run(&None::), "null"); +} + +#[test] +fn vec() { + assert_eq!(run(&vec![5u8, 6u8]), "[5,6]"); +} + +#[test] +fn tuple() { + assert_eq!(run(&(5u8, 6u8)), "[5,6]"); +} + +#[test] +fn tuple_struct() { + #[derive(Serialize)] + struct TupleStruct(u8, u8); + + assert_eq!(run(&TupleStruct(5u8, 6u8)), "[5,6]"); +} + +#[test] +fn map() { + use std::collections::BTreeMap; + + let mut map = BTreeMap::new(); + map.insert("a", 5u8); + map.insert("b", 6u8); + + assert_eq!(run(&map), r#"{"a":5,"b":6}"#); +} + +#[test] +fn struct_() { + #[derive(Serialize)] + struct Struct { + a: u8, + b: u8, + } + + assert_eq!(run(&Struct { a: 5u8, b: 6u8 }), r#"{"a":5,"b":6}"#); +} + +#[test] +fn struct_with_lifetime() { + #[derive(Serialize)] + struct Struct<'a> { + a: &'a str, + b: &'a str, + } + + assert_eq!( + run(&Struct { a: "abc", b: "def" }), + r#"{"a":"abc","b":"def"}"# + ); +} + +#[test] +fn struct_with_lifetime_and_lifetime_in_type() { + #[derive(Serialize)] + struct Struct<'a> { + a: &'a str, + b: &'a str, + c: std::marker::PhantomData<&'a ()>, + } + + assert_eq!( + run(&Struct { + a: "abc", + b: "def", + c: std::marker::PhantomData, + }), + r#"{"a":"abc","b":"def","c":null}"# + ); +} + +#[test] +fn zero_sized_struct() { + #[derive(Serialize)] + struct Struct; + + assert_eq!(run(&Struct), r#"null"#); +} + +#[test] +fn enum_() { + #[derive(Serialize)] + enum Enum { + A, + B, + } + + assert_eq!(run(&Enum::A), r#""A""#); + assert_eq!(run(&Enum::B), r#""B""#); +} + +#[test] +fn enum_tuple() { + #[derive(Serialize)] + enum Enum { + A(u8), + B(u8), + } + + assert_eq!(run(&Enum::A(5u8)), r#"{"A":5}"#); + assert_eq!(run(&Enum::B(6u8)), r#"{"B":6}"#); +} + +#[test] +fn enum_struct() { + #[derive(Serialize)] + enum Enum { + A { a: u8 }, + B { b: u8 }, + } + + assert_eq!(run(&Enum::A { a: 5u8 }), r#"{"A":{"a":5}}"#); + assert_eq!(run(&Enum::B { b: 6u8 }), r#"{"B":{"b":6}}"#); +} + +#[test] +fn enum_with_lifetime() { + #[derive(Serialize)] + enum Enum<'a> { + A { a: &'a str }, + B { b: &'a str }, + } + + assert_eq!(run(&Enum::A { a: "abc" }), r#"{"A":{"a":"abc"}}"#); + assert_eq!(run(&Enum::B { b: "def" }), r#"{"B":{"b":"def"}}"#); +} + +#[test] +fn enum_with_lifetime_and_lifetime_in_type() { + #[derive(Serialize)] + enum Enum<'a> { + A { a: &'a str }, + B { b: &'a str }, + C(PhantomData<&'a ()>), + } + + assert_eq!(run(&Enum::A { a: "abc" }), r#"{"A":{"a":"abc"}}"#); + assert_eq!(run(&Enum::B { b: "def" }), r#"{"B":{"b":"def"}}"#); +} + +#[test] +fn vec_element_error() { + struct Element; + + static COUNT: AtomicUsize = AtomicUsize::new(0); + + impl Serialize for Element { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let next = COUNT.fetch_add(1, Ordering::SeqCst); + + if next == 1 { + Err(Error::custom("failure")) + } else { + next.serialize(serializer) + } + } + } + + assert_eq!( + run_serialize_error(&vec![Element, Element, Element]), + ExecutionError::Serialize + ); +} + +#[test] +fn map_key_error() { + struct Key; + + impl Serialize for Key { + fn serialize(&self, _: S) -> Result + where + S: Serializer, + { + Err(Error::custom("failure")) + } + } + + struct Map; + + impl Serialize for Map { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_key(&Key)?; + map.serialize_value(&5u8)?; + map.end() + } + } + + assert_eq!(run_serialize_error(&Map), ExecutionError::Serialize); +} + +#[test] +fn map_value_error() { + struct Value; + + impl Serialize for Value { + fn serialize(&self, _: S) -> Result + where + S: Serializer, + { + Err(Error::custom("failure")) + } + } + + struct Map; + + impl Serialize for Map { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_key(&5u8)?; + map.serialize_value(&Value)?; + map.end() + } + } + + assert_eq!(run_serialize_error(&Map), ExecutionError::Serialize); +} + +#[test] +fn map_entry_key_error() { + struct Key; + + impl Serialize for Key { + fn serialize(&self, _: S) -> Result + where + S: Serializer, + { + Err(Error::custom("failure")) + } + } + + struct Map; + + impl Serialize for Map { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry(&Key, &5u8)?; + map.end() + } + } + + assert_eq!(run_serialize_error(&Map), ExecutionError::Serialize); +} + +#[test] +fn map_entry_value_error() { + struct Value; + + impl Serialize for Value { + fn serialize(&self, _: S) -> Result + where + S: Serializer, + { + Err(Error::custom("failure")) + } + } + + struct Map; + + impl Serialize for Map { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry(&5u8, &Value)?; + map.end() + } + } + + assert_eq!(run_serialize_error(&Map), ExecutionError::Serialize); +} + +#[test] +fn map_entry_error() { + struct Key; + + impl Serialize for Key { + fn serialize(&self, _: S) -> Result + where + S: Serializer, + { + Err(Error::custom("failure")) + } + } + + struct Value; + + impl Serialize for Value { + fn serialize(&self, _: S) -> Result + where + S: Serializer, + { + Err(Error::custom("failure")) + } + } + + struct Map; + + impl Serialize for Map { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry(&Key, &Value)?; + map.end() + } + } + + assert_eq!(run_serialize_error(&Map), ExecutionError::Serialize); +} + +#[test] +fn map_no_corresponding_value_error() { + struct Map; + + impl Serialize for Map { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_key(&5u8)?; + map.end() + } + } + + assert_eq!(run_serialize_error(&Map), ExecutionError::Serialize); +} + +#[test] +fn map_extra_value_error() { + struct Map; + + impl Serialize for Map { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_key(&5u8)?; + map.serialize_value(&5u8)?; + map.serialize_value(&5u8)?; + map.end() + } + } + + assert_eq!(run_serialize_error(&Map), ExecutionError::Serialize); +} diff --git a/src/lib.rs b/src/lib.rs index aeaeab5..f5255b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ //! quick-js is a a Rust wrapper for [QuickJS](https://bellard.org/quickjs/), a new Javascript //! engine by Fabrice Bellard. //! -//! It enables easy and straight-forward execution of modern Javascript from Rust. +//! It enables easy and straight-forward execution of modern Javascript from +//! Rust. //! //! ## Limitations //! @@ -42,12 +43,12 @@ mod value; #[cfg(test)] mod tests; -use std::{convert::TryFrom, error, fmt}; +use std::convert::TryFrom; +use std::{error, fmt}; -pub use self::{ - callback::{Arguments, Callback}, - value::*, -}; +pub use self::callback::{Arguments, Callback}; +pub use self::value::*; +use crate::bindings::OwnedJsValue; /// Error on Javascript execution. #[derive(PartialEq, Debug)] @@ -62,6 +63,9 @@ pub enum ExecutionError { Exception(JsValue), /// JS Runtime exceeded the memory limit. OutOfMemory, + // TODO: temp + /// Serialization Error + Serialize, #[doc(hidden)] __NonExhaustive, } @@ -74,6 +78,7 @@ impl fmt::Display for ExecutionError { Conversion(e) => e.fmt(f), Internal(e) => write!(f, "Internal error: {}", e), Exception(e) => write!(f, "{:?}", e), + Serialize => write!(f, "Serialization error"), OutOfMemory => write!(f, "Out of memory: runtime memory limit exceeded"), __NonExhaustive => unreachable!(), } @@ -182,7 +187,8 @@ impl Context { Self { wrapper } } - /// Create a `ContextBuilder` that allows customization of JS Runtime settings. + /// Create a `ContextBuilder` that allows customization of JS Runtime + /// settings. /// /// For details, see the methods on `ContextBuilder`. /// @@ -304,6 +310,34 @@ impl Context { Ok(()) } + /// Set a global variable using serde serialization. + /// + /// ```rust + /// use quick_js::{Context, JsValue}; + /// let context = Context::new().unwrap(); + /// + /// context.set_global_serde("someGlobalVariable", &42).unwrap(); + /// let value = context.eval_as::("someGlobalVariable").unwrap(); + /// assert_eq!( + /// value, + /// 42, + /// ); + /// ``` + #[cfg(feature = "serde")] + pub fn set_global_serde(&self, name: &str, value: &V) -> Result<(), ExecutionError> + where + V: serde::Serialize, + { + let global = self.wrapper.global()?; + + let mut context = quick_js_serde::Context::new_without_drop(self.wrapper.context); + let value = quick_js_serde::serialize(value, &mut context) + .map_err(|_| ExecutionError::Serialize)?; + let value = OwnedJsValue::new(&self.wrapper, value); + + global.set_property(name, value) + } + /// Call a global function in the Javascript namespace. /// /// **Promises**: @@ -348,8 +382,8 @@ impl Context { /// * must return a value /// * the return value must either: /// - be convertible to JsValue - /// - be a Result where T is convertible to JsValue - /// if Err(e) is returned, a Javascript exception will be raised + /// - be a Result where T is convertible to JsValue if Err(e) is + /// returned, a Javascript exception will be raised /// /// ```rust /// use quick_js::{Context, JsValue};