Skip to content

Commit

Permalink
Switch to rquickjs (#618)
Browse files Browse the repository at this point in the history
* chore: Initial migration to rquickjs

* More progress on migration:

Migrated the `Console` and `Random` APIs; got to the `StreamIO` API.

* Migrate all of the APIs to use rquickjs

This commit migrates the `console`, `stream_io`, `random` and `text_encoding`  APIs to use rquickjs. 

One notable change in this commit is the introduction of the `Args` struct to tie the lifetime of `Ctx<'js>` and `Rest<Value<'js>>` arguments, given that explicit lifetime binding is not possible in Rust closures at the moment.

* Finalize migration of the toolchain as a whole

* Uncomment integration tests

* Throw `TypeError` when `fatal` decoding is specified.

* Fix clippy errors

* Update documentation where applicable and remove reference to older crates

* Handle converting from JS strings to Rust strings for text encoding

* Migrate serializers and deserializers to use rquickjs and move them under the javy crate

* Move quickcheck to the top level Cargo.toml 


 `cargo` doesn't seem to pickup the fact that `quickcheck` is only used in tests.

* Update versions and CHANGELOGs

* Audit dependencies

And prune unused ones

* Improve safety comment

* Port previous implementation of read sync

I wanted to avoid as much as possible using the underlying QuickJS unsafe APIs, but that introduced behavioral changes and bugs. So for now, I'm sticking to the previous implementation.

* Review comments

* Improve comment for why `ManuallyDrop`

* Use `ManuallyDrop` for Context

* Format comments where applicable

* Use rquickjs 0.6.0 through a fork

This commit introduces rquickjs 0.6.0 through a fork that contains Wasm specific performance improvements. We intend to upstream these improvements.

* Cargo vet config

Adds special policy for rquickjs given that we're currently in a fork.

* Remove comment, cargo vet doesn't like them
  • Loading branch information
saulecabrera authored May 2, 2024
1 parent 8f4468c commit 235b5a6
Show file tree
Hide file tree
Showing 30 changed files with 3,622 additions and 1,621 deletions.
1,357 changes: 715 additions & 642 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ wasmtime-wasi = "16"
wasi-common = "16"
anyhow = "1.0"
once_cell = "1.19"
javy = { path = "crates/javy", version = "2.2.1-alpha.1" }
javy = { path = "crates/javy", version = "3.0.0-alpha.1" }

[profile.release]
lto = true
Expand Down
5 changes: 1 addition & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ docs:
cargo doc --package=javy-cli --open
cargo doc --package=javy-core --open --target=wasm32-wasi

test-quickjs-wasm-rs:
cargo wasi test --package=quickjs-wasm-rs -- --nocapture

test-javy:
cargo wasi test --package=javy --features json,messagepack -- --nocapture

Expand All @@ -47,7 +44,7 @@ test-wpt:
npm install --prefix wpt
npm test --prefix wpt

tests: test-quickjs-wasm-rs test-javy test-apis test-core test-cli test-wpt
tests: test-javy test-apis test-core test-cli test-wpt

fmt: fmt-quickjs-wasm-sys fmt-quickjs-wasm-rs fmt-javy fmt-apis fmt-core fmt-cli

Expand Down
5 changes: 5 additions & 0 deletions crates/apis/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Rewrite the APIs on top of Javy v3.0.0, which drops support for
`quickjs-wasm-rs` in favor of `rquickjs`

## [2.2.0] - 2024-01-31

### Changed
Expand Down
2 changes: 1 addition & 1 deletion crates/apis/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "javy-apis"
version = "2.2.1-alpha.1"
version = "3.0.0-alpha.1"
authors.workspace = true
edition.workspace = true
license.workspace = true
Expand Down
15 changes: 8 additions & 7 deletions crates/apis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ APIs are registered by enabling crate features.
## Example usage

```rust
use javy::{quickjs::JSValue, Runtime};
// with `console` feature enabled

// With the `console` feature enabled.
use javy::{Runtime, from_js_error};
use javy_apis::RuntimeExt;
use anyhow::Result;

fn main() -> Result<()> {
let runtime = Runtime::new_with_defaults()?;
let context = runtime.context();
context.eval_global("hello.js", "console.log('hello!');")?;
context.with(|cx| {
cx.eval_with_options(Default::default(), "console.log('hello!');")
.map_err(|e| to_js_error(cx.clone(), e))?
});
Ok(())
}
```
Expand All @@ -30,7 +35,3 @@ If you want to customize the runtime or the APIs, you can use the `Runtime::new_
## Publishing to crates.io

To publish this crate to crates.io, run `./publish.sh`.

## Using a custom WASI SDK

This crate can be compiled using a custom [WASI SDK](https://github.com/WebAssembly/wasi-sdk). When building this crate, set the `QUICKJS_WASM_SYS_WASI_SDK_PATH` environment variable to the absolute path where you installed the SDK.
150 changes: 86 additions & 64 deletions crates/apis/src/console/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::io::Write;

use anyhow::Result;
use anyhow::{Error, Result};
use javy::{
quickjs::{JSContextRef, JSValue, JSValueRef},
Runtime,
hold, hold_and_release, print,
quickjs::{prelude::MutFn, Context, Function, Object, Value},
to_js_error, Args, Runtime,
};

use crate::{APIConfig, JSApiSet};
Expand Down Expand Up @@ -31,50 +32,65 @@ impl JSApiSet for Console {
}
}

fn register_console<T, U>(context: &JSContextRef, log_stream: T, error_stream: U) -> Result<()>
fn register_console<T, U>(context: &Context, mut log_stream: T, mut error_stream: U) -> Result<()>
where
T: Write + 'static,
U: Write + 'static,
{
let console_log_callback = context.wrap_callback(console_log_to(log_stream))?;
let console_error_callback = context.wrap_callback(console_log_to(error_stream))?;
let console_object = context.object_value()?;
console_object.set_property("log", console_log_callback)?;
console_object.set_property("error", console_error_callback)?;
context
.global_object()?
.set_property("console", console_object)?;
context.with(|this| {
let globals = this.globals();
let console = Object::new(this.clone())?;

console.set(
"log",
Function::new(
this.clone(),
MutFn::new(move |cx, args| {
let (cx, args) = hold_and_release!(cx, args);
log(hold!(cx.clone(), args), &mut log_stream).map_err(|e| to_js_error(cx, e))
}),
)?,
)?;

console.set(
"error",
Function::new(
this.clone(),
MutFn::new(move |cx, args| {
let (cx, args) = hold_and_release!(cx, args);
log(hold!(cx.clone(), args), &mut error_stream).map_err(|e| to_js_error(cx, e))
}),
)?,
)?;

globals.set("console", console)?;
Ok::<_, Error>(())
})?;
Ok(())
}

fn console_log_to<T>(
mut stream: T,
) -> impl FnMut(&JSContextRef, JSValueRef, &[JSValueRef]) -> Result<JSValue>
where
T: Write + 'static,
{
move |_ctx: &JSContextRef, _this: JSValueRef, args: &[JSValueRef]| {
// Write full string to in-memory destination before writing to stream since each write call to the stream
// will invoke a hostcall.
let mut log_line = String::new();
for (i, arg) in args.iter().enumerate() {
if i != 0 {
log_line.push(' ');
}
let line = arg.to_string();
log_line.push_str(&line);
fn log<'js, T: Write>(args: Args<'js>, stream: &mut T) -> Result<Value<'js>> {
let (ctx, args) = args.release();
let mut buf = String::new();
for (i, arg) in args.iter().enumerate() {
if i != 0 {
buf.push(' ');
}
print(arg, &mut buf)?;
}

writeln!(stream, "{log_line}")?;
writeln!(stream, "{buf}")?;

Ok(JSValue::Undefined)
}
Ok(Value::new_undefined(ctx.clone()))
}

#[cfg(test)]
mod tests {
use anyhow::Result;
use javy::Runtime;
use anyhow::{Error, Result};
use javy::{
quickjs::{Object, Value},
Runtime,
};
use std::cell::RefCell;
use std::rc::Rc;
use std::{cmp, io};
Expand All @@ -88,9 +104,13 @@ mod tests {
fn test_register() -> Result<()> {
let runtime = Runtime::default();
Console::new().register(&runtime, &APIConfig::default())?;
let console = runtime.context().global_object()?.get_property("console")?;
assert!(console.get_property("log").is_ok());
assert!(console.get_property("error").is_ok());
runtime.context().with(|cx| {
let console: Object<'_> = cx.globals().get("console")?;
assert!(console.get::<&str, Value<'_>>("log").is_ok());
assert!(console.get::<&str, Value<'_>>("error").is_ok());

Ok::<_, Error>(())
})?;
Ok(())
}

Expand All @@ -102,24 +122,25 @@ mod tests {
let ctx = runtime.context();
register_console(ctx, stream.clone(), stream.clone())?;

ctx.eval_global("main", "console.log(\"hello world\");")?;
assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice());
ctx.with(|this| {
this.eval("console.log(\"hello world\");")?;
assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice());
stream.clear();

stream.clear();
this.eval("console.log(\"bonjour\", \"le\", \"monde\")")?;
assert_eq!(b"bonjour le monde\n", stream.buffer.borrow().as_slice());

ctx.eval_global("main", "console.log(\"bonjour\", \"le\", \"monde\")")?;
assert_eq!(b"bonjour le monde\n", stream.buffer.borrow().as_slice());
stream.clear();

stream.clear();
this.eval("console.log(2.3, true, { foo: 'bar' }, null, undefined)")?;
assert_eq!(
b"2.3 true [object Object] null undefined\n",
stream.buffer.borrow().as_slice()
);

Ok::<_, Error>(())
})?;

ctx.eval_global(
"main",
"console.log(2.3, true, { foo: 'bar' }, null, undefined)",
)?;
assert_eq!(
b"2.3 true [object Object] null undefined\n",
stream.buffer.borrow().as_slice()
);
Ok(())
}

Expand All @@ -131,24 +152,25 @@ mod tests {
let ctx = runtime.context();
register_console(ctx, stream.clone(), stream.clone())?;

ctx.eval_global("main", "console.error(\"hello world\");")?;
assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice());
ctx.with(|this| {
this.eval("console.error(\"hello world\");")?;
assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice());

stream.clear();
stream.clear();

ctx.eval_global("main", "console.error(\"bonjour\", \"le\", \"monde\")")?;
assert_eq!(b"bonjour le monde\n", stream.buffer.borrow().as_slice());
this.eval("console.error(\"bonjour\", \"le\", \"monde\")")?;
assert_eq!(b"bonjour le monde\n", stream.buffer.borrow().as_slice());

stream.clear();
stream.clear();

this.eval("console.error(2.3, true, { foo: 'bar' }, null, undefined)")?;
assert_eq!(
b"2.3 true [object Object] null undefined\n",
stream.buffer.borrow().as_slice()
);
Ok::<_, Error>(())
})?;

ctx.eval_global(
"main",
"console.error(2.3, true, { foo: 'bar' }, null, undefined)",
)?;
assert_eq!(
b"2.3 true [object Object] null undefined\n",
stream.buffer.borrow().as_slice()
);
Ok(())
}

Expand Down
47 changes: 32 additions & 15 deletions crates/apis/src/random/mod.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
use anyhow::Result;
use javy::{quickjs::JSValue, Runtime};
use anyhow::{Error, Result};
use javy::{
quickjs::{prelude::Func, Object},
Runtime,
};

use crate::{APIConfig, JSApiSet};

pub struct Random;

impl JSApiSet for Random {
fn register(&self, runtime: &Runtime, _config: &APIConfig) -> Result<()> {
let ctx = runtime.context();
ctx.global_object()?.get_property("Math")?.set_property(
"random",
// TODO figure out if we can lazily initialize the PRNG
ctx.wrap_callback(|_ctx, _this, _args| Ok(JSValue::Float(fastrand::f64())))?,
)?;
runtime.context().with(|cx| {
let globals = cx.globals();
let math: Object<'_> = globals.get("Math").expect("Math global to be defined");
math.set("random", Func::from(fastrand::f64))?;

Ok::<_, Error>(())
})?;

Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::{random::Random, APIConfig, JSApiSet};
use anyhow::Result;
use javy::Runtime;
use anyhow::{Error, Result};
use javy::{
quickjs::{context::EvalOptions, Value},
Runtime,
};

#[test]
fn test_random() -> Result<()> {
let runtime = Runtime::default();
Random.register(&runtime, &APIConfig::default())?;
let ctx = runtime.context();
ctx.eval_global("test.js", "result = Math.random()")?;
let result = ctx.global_object()?.get_property("result")?.as_f64()?;
assert!(result >= 0.0);
assert!(result < 1.0);
runtime.context().with(|this| {
let mut eval_opts = EvalOptions::default();
eval_opts.strict = false;
this.eval_with_options("result = Math.random()", eval_opts)?;
let result: f64 = this
.globals()
.get::<&str, Value<'_>>("result")?
.as_float()
.unwrap();
assert!(result >= 0.0);
assert!(result < 1.0);
Ok::<_, Error>(())
})?;

Ok(())
}
}
1 change: 1 addition & 0 deletions crates/apis/src/runtime_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ impl RuntimeExt for Runtime {
fn new_with_apis(config: Config, api_config: APIConfig) -> Result<Runtime> {
let runtime = Runtime::new(config)?;
crate::add_to_runtime(&runtime, api_config)?;

Ok(runtime)
}

Expand Down
Loading

0 comments on commit 235b5a6

Please sign in to comment.