Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serde support #127

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,30 @@ authors = ["Christoph Herzog <chris@theduke.at>"]
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"
]

41 changes: 41 additions & 0 deletions examples/serde.rs
Original file line number Diff line number Diff line change
@@ -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<Inner>,
}

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);
}
54 changes: 44 additions & 10 deletions libquickjs-sys/src/static-functions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

extern "C" {
fn JS_ValueGetTag_real(v: JSValue) -> i32;
fn JS_DupValue_real(ctx: *mut JSContext, v: JSValue);
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
16 changes: 16 additions & 0 deletions serde/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
34 changes: 34 additions & 0 deletions serde/src/context.rs
Original file line number Diff line number Diff line change
@@ -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 _) };
}
}
}
119 changes: 119 additions & 0 deletions serde/src/errors.rs
Original file line number Diff line number Diff line change
@@ -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<String, Internal> {
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<String, Internal> {
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<JSValue, Self> {
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<T: std::fmt::Display>(msg: T) -> Self {
Self::Unknown(msg.to_string())
}
}
19 changes: 19 additions & 0 deletions serde/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
mod context;
mod errors;
mod ser;

pub use context::Context;
use libquickjs_sys::JSValue;
use serde::Serialize;

pub fn serialize<T: ?Sized>(
value: &T,
context: &mut Context,
) -> Result<JSValue, errors::SerializationError>
where
T: Serialize,
{
let serializer = ser::Serializer::new(context);

value.serialize(serializer)
}
Loading