Skip to content

Commit

Permalink
Introduce serde_v8 (denoland#9722)
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronO committed Mar 26, 2021
1 parent e795441 commit 3d2e05d
Show file tree
Hide file tree
Showing 18 changed files with 1,887 additions and 102 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"cli",
"core",
"runtime",
"serde_v8",
"test_plugin",
"test_util",
"op_crates/crypto",
Expand Down Expand Up @@ -42,4 +43,4 @@ opt-level = 3
[profile.release.package.async-compression]
opt-level = 3
[profile.release.package.brotli-decompressor]
opt-level = 3
opt-level = 3
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pin-project = "1.0.5"
rusty_v8 = "0.21.0"
serde = { version = "1.0.123", features = ["derive"] }
serde_json = { version = "1.0.62", features = ["preserve_order"] }
serde_v8 = { version = "0.0.0", path = "../serde_v8" }
smallvec = "1.6.1"
url = { version = "2.2.0", features = ["serde"] }

Expand Down
144 changes: 43 additions & 101 deletions core/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use std::option::Option;
use url::Url;
use v8::MapFnTo;

use serde::Serialize;
use serde_v8::to_v8;

lazy_static! {
pub static ref EXTERNAL_REFERENCES: v8::ExternalReferences =
v8::ExternalReferences::new(&[
Expand Down Expand Up @@ -477,16 +480,17 @@ fn eval_context(
let url = v8::Local::<v8::String>::try_from(args.get(1))
.map(|n| Url::from_file_path(n.to_rust_string_lossy(scope)).unwrap());

let output = v8::Array::new(scope, 2);
/*
output[0] = result
output[1] = ErrorInfo | null
ErrorInfo = {
thrown: Error | any,
isNativeError: boolean,
isCompileError: boolean,
}
*/
#[derive(Serialize)]
struct Output<'s>(Option<serde_v8::Value<'s>>, Option<ErrInfo<'s>>);

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ErrInfo<'s> {
thrown: serde_v8::Value<'s>,
is_native_error: bool,
is_compile_error: bool,
}

let tc_scope = &mut v8::TryCatch::new(scope);
let name = v8::String::new(
tc_scope,
Expand All @@ -499,39 +503,15 @@ fn eval_context(
if maybe_script.is_none() {
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();

let js_zero = v8::Integer::new(tc_scope, 0);
let js_null = v8::null(tc_scope);
output.set(tc_scope, js_zero.into(), js_null.into());

let errinfo_obj = v8::Object::new(tc_scope);

let is_compile_error_key =
v8::String::new(tc_scope, "isCompileError").unwrap();
let is_compile_error_val = v8::Boolean::new(tc_scope, true);
errinfo_obj.set(
tc_scope,
is_compile_error_key.into(),
is_compile_error_val.into(),
);

let is_native_error_key =
v8::String::new(tc_scope, "isNativeError").unwrap();
let is_native_error_val =
v8::Boolean::new(tc_scope, exception.is_native_error());
errinfo_obj.set(
tc_scope,
is_native_error_key.into(),
is_native_error_val.into(),
let output = Output(
None,
Some(ErrInfo {
thrown: exception.into(),
is_native_error: exception.is_native_error(),
is_compile_error: true,
}),
);

let thrown_key = v8::String::new(tc_scope, "thrown").unwrap();
errinfo_obj.set(tc_scope, thrown_key.into(), exception);

let js_one = v8::Integer::new(tc_scope, 1);
output.set(tc_scope, js_one.into(), errinfo_obj.into());

rv.set(output.into());
rv.set(to_v8(tc_scope, output).unwrap());
return;
}

Expand All @@ -540,48 +520,20 @@ fn eval_context(
if result.is_none() {
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();

let js_zero = v8::Integer::new(tc_scope, 0);
let js_null = v8::null(tc_scope);
output.set(tc_scope, js_zero.into(), js_null.into());

let errinfo_obj = v8::Object::new(tc_scope);

let is_compile_error_key =
v8::String::new(tc_scope, "isCompileError").unwrap();
let is_compile_error_val = v8::Boolean::new(tc_scope, false);
errinfo_obj.set(
tc_scope,
is_compile_error_key.into(),
is_compile_error_val.into(),
);

let is_native_error_key =
v8::String::new(tc_scope, "isNativeError").unwrap();
let is_native_error_val =
v8::Boolean::new(tc_scope, exception.is_native_error());
errinfo_obj.set(
tc_scope,
is_native_error_key.into(),
is_native_error_val.into(),
let output = Output(
None,
Some(ErrInfo {
thrown: exception.into(),
is_native_error: exception.is_native_error(),
is_compile_error: false,
}),
);

let thrown_key = v8::String::new(tc_scope, "thrown").unwrap();
errinfo_obj.set(tc_scope, thrown_key.into(), exception);

let js_one = v8::Integer::new(tc_scope, 1);
output.set(tc_scope, js_one.into(), errinfo_obj.into());

rv.set(output.into());
rv.set(to_v8(tc_scope, output).unwrap());
return;
}

let js_zero = v8::Integer::new(tc_scope, 0);
let js_one = v8::Integer::new(tc_scope, 1);
let js_null = v8::null(tc_scope);
output.set(tc_scope, js_zero.into(), result.unwrap());
output.set(tc_scope, js_one.into(), js_null.into());
rv.set(output.into());
let output = Output(Some(result.unwrap().into()), None);
rv.set(to_v8(tc_scope, output).unwrap());
}

fn encode(
Expand Down Expand Up @@ -850,30 +802,24 @@ fn get_promise_details(
}
};

let promise_details = v8::Array::new(scope, 2);
#[derive(Serialize)]
struct PromiseDetails<'s>(u32, Option<serde_v8::Value<'s>>);

match promise.state() {
v8::PromiseState::Pending => {
let js_zero = v8::Integer::new(scope, 0);
promise_details.set(scope, js_zero.into(), js_zero.into());
rv.set(promise_details.into());
rv.set(to_v8(scope, PromiseDetails(0, None)).unwrap());
}
v8::PromiseState::Fulfilled => {
let js_zero = v8::Integer::new(scope, 0);
let js_one = v8::Integer::new(scope, 1);
let promise_result = promise.result(scope);
promise_details.set(scope, js_zero.into(), js_one.into());
promise_details.set(scope, js_one.into(), promise_result);
rv.set(promise_details.into());
rv.set(
to_v8(scope, PromiseDetails(1, Some(promise_result.into()))).unwrap(),
);
}
v8::PromiseState::Rejected => {
let js_zero = v8::Integer::new(scope, 0);
let js_one = v8::Integer::new(scope, 1);
let js_two = v8::Integer::new(scope, 2);
let promise_result = promise.result(scope);
promise_details.set(scope, js_zero.into(), js_two.into());
promise_details.set(scope, js_one.into(), promise_result);
rv.set(promise_details.into());
rv.set(
to_v8(scope, PromiseDetails(2, Some(promise_result.into()))).unwrap(),
);
}
}
}
Expand Down Expand Up @@ -912,14 +858,10 @@ fn get_proxy_details(
}
};

let proxy_details = v8::Array::new(scope, 2);
let js_zero = v8::Integer::new(scope, 0);
let js_one = v8::Integer::new(scope, 1);
let target = proxy.get_target(scope);
let handler = proxy.get_handler(scope);
proxy_details.set(scope, js_zero.into(), target);
proxy_details.set(scope, js_one.into(), handler);
rv.set(proxy_details.into());
let p: (serde_v8::Value, serde_v8::Value) = (target.into(), handler.into());
rv.set(to_v8(scope, p).unwrap());
}

fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef<str>) {
Expand Down
16 changes: 16 additions & 0 deletions serde_v8/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "serde_v8"
version = "0.0.0"
authors = ["Aaron O'Mullan <[email protected]>"]
edition = "2018"


[dependencies]
serde = { version = "1.0.123", features = ["derive"] }
rusty_v8 = "0.21.0"

[dev-dependencies]
serde_json = "1.0.62"

[[example]]
name = "basic"
55 changes: 55 additions & 0 deletions serde_v8/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# serde_v8

Serde support for encoding/decoding (rusty_)v8 values.

Broadly `serde_v8` aims to provide an expressive but ~maximally efficient
encoding layer to biject rust & v8/js values. It's a core component of deno's
op-layer and is used to encode/decode all non-buffer values.

**Original issue:**
[denoland/deno#9540](https://github.com/denoland/deno/issues/9540)

## Quickstart

`serde_v8` fits naturally into the serde ecosystem, so if you've already used
`serde` or `serde_json`, `serde_v8`'s API should be very familiar.

`serde_v8` exposes two key-functions:

- `to_v8`: maps `rust->v8`, similar to `serde_json::to_string`, ...
- `from_v8`: maps `v8->rust`, similar to `serde_json::from_str`, ...

## Best practices

Whilst `serde_v8` is compatible with `serde_json::Value` it's important to keep
in mind that `serde_json::Value` is essentially a loosely-typed value (think
nested HashMaps), so when writing ops we recommend directly using rust
structs/tuples or primitives, since mapping to `serde_json::Value` will add
extra overhead and result in slower ops.

I also recommend avoiding unecessary "wrappers", if your op takes a single-keyed
struct, consider unwrapping that as a plain value unless you plan to add fields
in the near-future.

Instead of returning "nothing" via `Ok(json!({}))`, change your return type to
rust's unit type `()` and returning `Ok(())`, `serde_v8` will efficiently encode
that as a JS `null`.

## Advanced features

If you need to mix rust & v8 values in structs/tuples, you can use the special
`serde_v8::Value` type, which will passthrough the original v8 value untouched
when encoding/decoding.

## TODO

- [ ] Experiment with KeyCache to optimize struct keys
- [ ] Experiment with external v8 strings
- [ ] Explore using
[json-stringifier.cc](https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/json/json-stringifier.cc)'s
fast-paths for arrays
- [ ] Improve tests to test parity with `serde_json` (should be mostly
interchangeable)
- [ ] Consider a `Payload` type that's deserializable by itself (holds scope &
value)
- [ ] Ensure we return errors instead of panicking on `.unwrap()`s
57 changes: 57 additions & 0 deletions serde_v8/examples/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use rusty_v8 as v8;

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct MathOp {
pub a: u64,
pub b: u64,
pub operator: Option<String>,
}

fn main() {
let platform = v8::new_default_platform().unwrap();
v8::V8::initialize_platform(platform);
v8::V8::initialize();

{
let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
let handle_scope = &mut v8::HandleScope::new(isolate);
let context = v8::Context::new(handle_scope);
let scope = &mut v8::ContextScope::new(handle_scope, context);

fn exec<'s>(
scope: &mut v8::HandleScope<'s>,
src: &str,
) -> v8::Local<'s, v8::Value> {
let code = v8::String::new(scope, src).unwrap();
let script = v8::Script::compile(scope, code, None).unwrap();
script.run(scope).unwrap()
}

let v = exec(scope, "32");
let x32: u64 = serde_v8::from_v8(scope, v).unwrap();
println!("x32 = {}", x32);

let v = exec(scope, "({a: 1, b: 3, c: 'ignored'})");
let mop: MathOp = serde_v8::from_v8(scope, v).unwrap();
println!("mop = {:?}", mop);

let v = exec(scope, "[1,2,3,4,5]");
let arr: Vec<u64> = serde_v8::from_v8(scope, v).unwrap();
println!("arr = {:?}", arr);

let v = exec(scope, "['hello', 'world']");
let hi: Vec<String> = serde_v8::from_v8(scope, v).unwrap();
println!("hi = {:?}", hi);

let v: v8::Local<v8::Value> = v8::Number::new(scope, 12345.0).into();
let x: f64 = serde_v8::from_v8(scope, v).unwrap();
println!("x = {}", x);
}

unsafe {
v8::V8::dispose();
}
v8::V8::shutdown_platform();
}
Loading

0 comments on commit 3d2e05d

Please sign in to comment.