From 30c21d080e2c513e64cbeb4ada6dc6e2d0a7da03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Cabrera?= Date: Tue, 14 May 2024 14:22:35 -0400 Subject: [PATCH] Refactor the structure of the `Config`/APIs (#646) * Refactor the structure of the `Config`/APIs This change primarily rewrites the current APIs using "Intrinsics" as defined by rquickjs. This change has several objectives: Refactor the structure of the Config and its relationship with the Runtime to pave the way for a truly extensible and configurable engine. The way Intrinsics are exposed in rquickjs simplifies (i) enabling a granular configuration experience of the native intrinsics, which was not entirely clear how to achieve before, and (ii) adding custom APIs as intrinsics. Enhance how our configuration operates: the previous model assumed an almost static configuration setup, allowing only limited, non-straightforward customizability from the CLI in the future. This was mostly modeled per API and not as a global aspect of the runtime. Our recommended approach for users needing functionality beyond what was provided was to fork or recreate their own version of the CLI/APIs. A telling sign of this issue is that the configuration parameters were almost never used in any of the APIs. This PR paves the way for greater extensibility, which will be facilitated by threading configuration options from the CLI in a follow-up PR; this method will also allow for opting-in to non-standard functionality via a CLI flag, helping us avoid issues like the one noted here: https://github.com/bytecodealliance/javy/issues/628. Along the way, I had to make several changes to make all this work; the most relevant ones are: (i) deprecating the `javy-apis` crate (ii) enabling all the APIs by default and (iii) reworking a bit the `Console` implementation. The reasoning to deprecate the `javy-apis` crate is mostly around: (i) the fact that we're revamping our extensilbity model in the near fuiture and (ii) the complexity around cyclical dependencies: `javy` exposes rquickjs and given that the cofiguration is tied to the runtime, `javy` needed to depend on `javy-apis` but the other way around was also true. I decided not to spend too much cycles on this mostly given point (i) above. There still things to follow-up on after this PR, namely - [ ] Update all the documentation under `docs` - [ ] A PR to thread the engine configuration from the CLI. NB that the engine behaviour is not altered in this PR. * Fix typos * Add a comment in the apis/lib.rs This makes `cargo fmt -- --check` happy * Review comments --- Cargo.lock | 8 +- Makefile | 5 +- crates/apis/CHANGELOG.md | 3 + crates/apis/Cargo.toml | 12 +- crates/apis/README.md | 37 +--- crates/apis/src/api_config.rs | 12 -- crates/apis/src/console/config.rs | 51 ------ crates/apis/src/lib.rs | 89 +--------- crates/apis/src/runtime_ext.rs | 36 ---- crates/core/Cargo.toml | 1 - crates/core/src/runtime.rs | 11 +- crates/javy/Cargo.toml | 2 + .../src => javy/src/apis}/console/mod.rs | 119 ++++++------- crates/javy/src/apis/mod.rs | 62 +++++++ .../{apis/src => javy/src/apis}/random/mod.rs | 33 ++-- .../src => javy/src/apis}/stream_io/io.js | 0 .../src => javy/src/apis}/stream_io/mod.rs | 72 ++++---- .../src/apis}/text_encoding/mod.rs | 76 ++++---- .../src/apis}/text_encoding/text-encoding.js | 0 crates/javy/src/config.rs | 166 +++++++++++++++++- crates/javy/src/lib.rs | 8 +- crates/javy/src/runtime.rs | 100 ++++++++++- 22 files changed, 489 insertions(+), 414 deletions(-) delete mode 100644 crates/apis/src/api_config.rs delete mode 100644 crates/apis/src/console/config.rs delete mode 100644 crates/apis/src/runtime_ext.rs rename crates/{apis/src => javy/src/apis}/console/mod.rs (72%) create mode 100644 crates/javy/src/apis/mod.rs rename crates/{apis/src => javy/src/apis}/random/mod.rs (56%) rename crates/{apis/src => javy/src/apis}/stream_io/io.js (100%) rename crates/{apis/src => javy/src/apis}/stream_io/mod.rs (75%) rename crates/{apis/src => javy/src/apis}/text_encoding/mod.rs (67%) rename crates/{apis/src => javy/src/apis}/text_encoding/text-encoding.js (100%) diff --git a/Cargo.lock b/Cargo.lock index 6d66cb4e..a5310d9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1486,6 +1486,8 @@ name = "javy" version = "3.0.0-alpha.1" dependencies = [ "anyhow", + "bitflags 2.5.0", + "fastrand", "quickcheck", "rmp-serde", "rquickjs", @@ -1497,11 +1499,6 @@ dependencies = [ [[package]] name = "javy-apis" version = "3.0.0-alpha.1" -dependencies = [ - "anyhow", - "fastrand", - "javy", -] [[package]] name = "javy-cli" @@ -1536,7 +1533,6 @@ version = "0.2.0" dependencies = [ "anyhow", "javy", - "javy-apis", "once_cell", ] diff --git a/Makefile b/Makefile index 9b0b5c2e..12af60b1 100644 --- a/Makefile +++ b/Makefile @@ -24,9 +24,6 @@ docs: test-javy: cargo wasi test --package=javy --features json,messagepack -- --nocapture -test-apis: - cargo hack wasi test --package=javy-apis --each-feature -- --nocapture - test-core: cargo wasi test --package=javy-core -- --nocapture @@ -44,7 +41,7 @@ test-wpt: npm install --prefix wpt npm test --prefix wpt -tests: test-javy test-apis test-core test-cli test-wpt +tests: test-javy test-core test-cli test-wpt fmt: fmt-quickjs-wasm-sys fmt-quickjs-wasm-rs fmt-javy fmt-apis fmt-core fmt-cli diff --git a/crates/apis/CHANGELOG.md b/crates/apis/CHANGELOG.md index 888bcc22..1b5a3012 100644 --- a/crates/apis/CHANGELOG.md +++ b/crates/apis/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Mark the crate as deprecated. +- Fold APIs under the [Javy crate](https://crates.io/crates/javy) to enable more + ergonomic runtime configuration. - Rewrite the APIs on top of Javy v3.0.0, which drops support for `quickjs-wasm-rs` in favor of `rquickjs` diff --git a/crates/apis/Cargo.toml b/crates/apis/Cargo.toml index e4854dfc..354f50dc 100644 --- a/crates/apis/Cargo.toml +++ b/crates/apis/Cargo.toml @@ -9,13 +9,7 @@ homepage = "https://github.com/bytecodealliance/javy/tree/main/crates/apis" repository = "https://github.com/bytecodealliance/javy/tree/main/crates/apis" categories = ["wasm"] -[features] -console = [] -random = ["dep:fastrand"] -stream_io = [] -text_encoding = [] - [dependencies] -anyhow = { workspace = true } -fastrand = { version = "2.1.0", optional = true } -javy = { workspace = true } + +[badges] +maintenance = { status = "deprecated" } diff --git a/crates/apis/README.md b/crates/apis/README.md index 8718cc5e..9ba72734 100644 --- a/crates/apis/README.md +++ b/crates/apis/README.md @@ -1,36 +1,3 @@ -
-

Javy APIs

-

- A collection of APIs for Javy -

+# This crate is deprecated. -

- Documentation Status - crates.io status -

-
- -Refer to the [crate level documentation](https://docs.rs/javy-apis) to learn more. - -Example usage: - -```rust -// 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.with(|cx| { - cx.eval_with_options(Default::default(), "console.log('hello!');") - .map_err(|e| to_js_error(cx.clone(), e))? - }); - Ok(()) -} -``` - -## Publishing to crates.io - -To publish this crate to crates.io, run `./publish.sh`. +Javy APIs have been folded into the [Javy crate](https://crates.io/crates/javy) diff --git a/crates/apis/src/api_config.rs b/crates/apis/src/api_config.rs deleted file mode 100644 index 0b0955ad..00000000 --- a/crates/apis/src/api_config.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// A configuration for APIs added in this crate. -/// -/// Example usage: -/// ``` -/// # use javy_apis::APIConfig; -/// let api_config = APIConfig::default(); -/// ``` -#[derive(Debug, Default)] -pub struct APIConfig { - #[cfg(feature = "console")] - pub(crate) console: crate::console::ConsoleConfig, -} diff --git a/crates/apis/src/console/config.rs b/crates/apis/src/console/config.rs deleted file mode 100644 index 3eb2152f..00000000 --- a/crates/apis/src/console/config.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::io::{self, Write}; - -use crate::APIConfig; - -/// A selection of possible destination streams for `console.log` and -/// `console.error`. -#[derive(Debug)] -pub enum LogStream { - /// The standard output stream. - StdOut, - /// The standard error stream. - StdErr, -} - -impl LogStream { - pub(super) fn to_stream(&self) -> Box { - match self { - Self::StdErr => Box::new(io::stderr()), - Self::StdOut => Box::new(io::stdout()), - } - } -} - -#[derive(Debug)] -pub(crate) struct ConsoleConfig { - pub(super) log_stream: LogStream, - pub(super) error_stream: LogStream, -} - -impl Default for ConsoleConfig { - fn default() -> Self { - Self { - log_stream: LogStream::StdOut, - error_stream: LogStream::StdErr, - } - } -} - -impl APIConfig { - /// Sets the destination stream for `console.log`. - pub fn log_stream(&mut self, stream: LogStream) -> &mut Self { - self.console.log_stream = stream; - self - } - - /// Sets the destination stream for `console.error`. - pub fn error_stream(&mut self, stream: LogStream) -> &mut Self { - self.console.error_stream = stream; - self - } -} diff --git a/crates/apis/src/lib.rs b/crates/apis/src/lib.rs index d9b30ce1..c554decf 100644 --- a/crates/apis/src/lib.rs +++ b/crates/apis/src/lib.rs @@ -1,87 +1,2 @@ -//! A collection of APIs for Javy. -//! -//! APIs are enabled through cargo features. -//! -//! Example usage: -//! ```rust -//! -//! //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.with(|cx| { -//! cx.eval_with_options(Default::default(), "console.log('hello!');") -//! .map_err(|e| to_js_error(cx.clone(), e))? -//! }); -//! Ok(()) -//! } -//! -//! ``` -//! -//! If you want to customize the runtime or the APIs, you can use the -//! [`Runtime::new_with_apis`] method instead to provide a [`javy::Config`] -//! for the underlying [`Runtime`] or an [`APIConfig`] for the APIs. -//! -//! ## Features -//! * `console`: Adds an implementation of the `console.log` and `console.error`, -//! enabling the configuration of the standard streams. -//! * `text_encoding`: Registers implementations of `TextEncoder` and `TextDecoder`. -//! * `random`: Overrides the implementation of `Math.random` to one that seeds -//! the RNG on first call to `Math.random`. This is helpful to enable when using -//! using a tool like Wizer to snapshot a [`Runtime`] so that the output of -//! `Math.random` relies on the WASI context used at runtime and not the WASI -//! context used when Wizening. Enabling this feature will increase the size of -//! the Wasm module that includes the Javy Runtime and will introduce an -//! additional hostcall invocation when `Math.random` is invoked for the first -//! time. -//! * `stream_io`: Adds an implementation of `Javy.IO.readSync` and `Javy.IO.writeSync`. - -use anyhow::Result; -use javy::Runtime; - -pub use api_config::APIConfig; -#[cfg(feature = "console")] -pub use console::LogStream; -pub use runtime_ext::RuntimeExt; - -mod api_config; -#[cfg(feature = "console")] -mod console; -#[cfg(feature = "random")] -mod random; -mod runtime_ext; -#[cfg(feature = "stream_io")] -mod stream_io; -#[cfg(feature = "text_encoding")] -mod text_encoding; - -pub(crate) trait JSApiSet { - fn register(&self, runtime: &Runtime, config: &APIConfig) -> Result<()>; -} - -/// Adds enabled JS APIs to the provided [`Runtime`]. -/// -/// ## Example -/// ``` -/// # use anyhow::Error; -/// # use javy::Runtime; -/// # use javy_apis::APIConfig; -/// let runtime = Runtime::default(); -/// javy_apis::add_to_runtime(&runtime, APIConfig::default())?; -/// # Ok::<(), Error>(()) -/// ``` -pub fn add_to_runtime(runtime: &Runtime, config: APIConfig) -> Result<()> { - #[cfg(feature = "console")] - console::Console::new().register(runtime, &config)?; - #[cfg(feature = "random")] - random::Random.register(runtime, &config)?; - #[cfg(feature = "stream_io")] - stream_io::StreamIO.register(runtime, &config)?; - #[cfg(feature = "text_encoding")] - text_encoding::TextEncoding.register(runtime, &config)?; - Ok(()) -} +//! Javy APIs -- Deprecated +//! Javy APIs moved to the main Javy crate. diff --git a/crates/apis/src/runtime_ext.rs b/crates/apis/src/runtime_ext.rs deleted file mode 100644 index c7382f9b..00000000 --- a/crates/apis/src/runtime_ext.rs +++ /dev/null @@ -1,36 +0,0 @@ -use anyhow::Result; -use javy::{Config, Runtime}; - -use crate::APIConfig; - -/// A extension trait for [`Runtime`] that creates a [`Runtime`] with APIs -/// provided in this crate. -/// -/// ## Example -/// ``` -/// # use anyhow::Error; -/// use javy::Runtime; -/// use javy_apis::RuntimeExt; -/// let runtime = Runtime::new_with_defaults()?; -/// # Ok::<(), Error>(()) -/// ``` -pub trait RuntimeExt { - /// Creates a [`Runtime`] configured by the provided [`Config`] with JS - /// APIs added configured according to the [`APIConfig`]. - fn new_with_apis(config: Config, api_config: APIConfig) -> Result; - /// Creates a [`Runtime`] with JS APIs added with a default configuration. - fn new_with_defaults() -> Result; -} - -impl RuntimeExt for Runtime { - fn new_with_apis(config: Config, api_config: APIConfig) -> Result { - let runtime = Runtime::new(config)?; - crate::add_to_runtime(&runtime, api_config)?; - - Ok(runtime) - } - - fn new_with_defaults() -> Result { - Self::new_with_apis(Config::default(), APIConfig::default()) - } -} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8781578f..7c0c9c01 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -16,7 +16,6 @@ crate-type = ["cdylib"] [dependencies] anyhow = { workspace = true } javy = { workspace = true, features = ["export_alloc_fns"] } -javy-apis = { path = "../apis", features = ["console", "text_encoding", "random", "stream_io"] } once_cell = { workspace = true } [features] diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index a2d5bcbb..86403a91 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -1,9 +1,12 @@ use anyhow::Result; use javy::{Config, Runtime}; -use javy_apis::{APIConfig, LogStream, RuntimeExt}; pub(crate) fn new_runtime() -> Result { - let mut api_config = APIConfig::default(); - api_config.log_stream(LogStream::StdErr); - Runtime::new_with_apis(Config::default(), api_config) + let mut config = Config::default(); + let config = config + .text_encoding(true) + .redirect_stdout_to_stderr(true) + .javy_stream_io(true); + + Runtime::new(std::mem::take(config)) } diff --git a/crates/javy/Cargo.toml b/crates/javy/Cargo.toml index 1f628e1f..02303c2f 100644 --- a/crates/javy/Cargo.toml +++ b/crates/javy/Cargo.toml @@ -19,6 +19,8 @@ rmp-serde = { version = "^1.3", optional = true } # TODO: cargo doesn't seem to pickup the fact that quickcheck is only used for # tests. quickcheck = "1" +bitflags = "2.5.0" +fastrand = "2.1.0" [features] export_alloc_fns = [] diff --git a/crates/apis/src/console/mod.rs b/crates/javy/src/apis/console/mod.rs similarity index 72% rename from crates/apis/src/console/mod.rs rename to crates/javy/src/apis/console/mod.rs index 970c27bd..c169c508 100644 --- a/crates/apis/src/console/mod.rs +++ b/crates/javy/src/apis/console/mod.rs @@ -1,73 +1,69 @@ -use std::io::Write; +use std::io::{stderr, stdout, Write}; +use std::ptr::NonNull; -use anyhow::{Error, Result}; -use javy::{ +use crate::{ hold, hold_and_release, quickjs::{ - convert, prelude::MutFn, Context, FromJs, Function, Object, String as JSString, Value, + context::Intrinsic, convert, prelude::MutFn, qjs, Ctx, FromJs, Function, Object, + String as JSString, Value, }, - to_js_error, to_string_lossy, Args, Runtime, + to_js_error, to_string_lossy, Args, }; +use anyhow::Result; -use crate::{APIConfig, JSApiSet}; +/// An implementation of JavaScript `console` APIs. +/// This implementation is *not* standard in the sense that it redirects the output of +/// `console.log` to stderr. +pub(crate) struct NonStandardConsole; -pub(super) use config::ConsoleConfig; -pub use config::LogStream; +/// An implemetation of JavaScript `console` APIs. This implementation is +/// standard as it redirects `console.log` to stdout and `console.error` to +/// stderr. +pub(crate) struct Console; -mod config; - -pub(super) struct Console {} - -impl Console { - pub(super) fn new() -> Self { - Console {} +impl Intrinsic for NonStandardConsole { + unsafe fn add_intrinsic(ctx: NonNull) { + register(Ctx::from_raw(ctx), stderr(), stderr()).expect("registering console to succeed"); } } -impl JSApiSet for Console { - fn register(&self, runtime: &Runtime, config: &APIConfig) -> Result<()> { - register_console( - runtime.context(), - config.console.log_stream.to_stream(), - config.console.error_stream.to_stream(), - ) +impl Intrinsic for Console { + unsafe fn add_intrinsic(ctx: NonNull) { + register(Ctx::from_raw(ctx), stdout(), stderr()).expect("registering console to succeed"); } } -fn register_console(context: &Context, mut log_stream: T, mut error_stream: U) -> Result<()> +pub(crate) fn register(this: Ctx<'_>, mut log_stream: T, mut error_stream: U) -> Result<()> where T: Write + 'static, U: Write + 'static, { - 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>(()) - })?; + 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(()) } @@ -104,24 +100,19 @@ fn log<'js, T: Write>(args: Args<'js>, stream: &mut T) -> Result> { #[cfg(test)] mod tests { - use anyhow::{Error, Result}; - use javy::{ + use crate::{ + apis::console::register, quickjs::{Object, Value}, Runtime, }; + use anyhow::{Error, Result}; use std::cell::RefCell; use std::rc::Rc; use std::{cmp, io}; - use crate::console::register_console; - use crate::{APIConfig, JSApiSet}; - - use super::Console; - #[test] fn test_register() -> Result<()> { let runtime = Runtime::default(); - Console::new().register(&runtime, &APIConfig::default())?; runtime.context().with(|cx| { let console: Object<'_> = cx.globals().get("console")?; assert!(console.get::<&str, Value<'_>>("log").is_ok()); @@ -135,12 +126,14 @@ mod tests { #[test] fn test_value_serialization() -> Result<()> { let mut stream = SharedStream::default(); - let runtime = Runtime::default(); let ctx = runtime.context(); - register_console(ctx, stream.clone(), stream.clone())?; ctx.with(|this| { + register(this.clone(), stream.clone(), stream.clone()).unwrap(); + this.eval("console.log(\"hello world\");")?; + assert_eq!(b"hello world\n", stream.buffer.borrow().as_slice()); + stream.clear(); macro_rules! test_console_log { ($js:expr, $expected:expr) => {{ this.eval($js)?; @@ -218,9 +211,9 @@ mod tests { let runtime = Runtime::default(); let ctx = runtime.context(); - register_console(ctx, log_stream.clone(), error_stream.clone())?; ctx.with(|this| { + register(this.clone(), log_stream.clone(), error_stream.clone()).unwrap(); this.eval("console.log(\"hello world\");")?; assert_eq!(b"hello world\n", log_stream.buffer.borrow().as_slice()); assert!(error_stream.buffer.borrow().is_empty()); diff --git a/crates/javy/src/apis/mod.rs b/crates/javy/src/apis/mod.rs new file mode 100644 index 00000000..0b9d18f4 --- /dev/null +++ b/crates/javy/src/apis/mod.rs @@ -0,0 +1,62 @@ +//! A collection of APIs for Javy. +//! +//! APIs are enabled through the the [`Config`](crate::Config) and are defined +//! in term of the [`Intrinsic`](rquickjs::context::Intrinsic) provided by +//! rquickjs. +//! +//! Example usage: +//! ```rust +//! +//! use javy::{Runtime, from_js_error}; +//! use javy_apis::RuntimeExt; +//! use anyhow::Result; +//! +//! fn main() -> Result<()> { +//! let mut config = Config::default(); +//! config.text_decoding(true); +//! let runtime = Runtime::new(config); +//! let context = runtime.context(); +//! context.with(|cx| { +//! cx.eval_with_options(Default::default(), r# +//! "console.log(new TextEncdoder().decode("")) +//! "#) +//! .map_err(|e| to_js_error(cx.clone(), e))? +//! }); +//! Ok(()) +//! } +//! +//! ``` +//! +//! ## Features +//! +//! ### `console` +//! +//! Adds an implementation of the `console.log` and `console.error`. +//! +//! ### `TextEncoding` +//! +//! Provides partial implementations of `TextEncoder` and `TextDecoder`. +//! Disables by default. +//! +//! ### `Random` +//! +//! Overrides the implementation of `Math.random` to one that seeds +//! the RNG on first call to `Math.random`. This is helpful to enable when using +//! using a tool like Wizer to snapshot a [`Runtime`] so that the output of +//! `Math.random` relies on the WASI context used at runtime and not the WASI +//! context used when snapshotting. +//! +//! ### `StreamIO` +//! +//! Provides an implementation of `Javy.IO.readSync` and `Javy.IO.writeSync`. +//! Disabled by default. + +pub(crate) mod console; +pub(crate) mod random; +pub(crate) mod stream_io; +pub(crate) mod text_encoding; + +pub(crate) use console::*; +pub(crate) use random::*; +pub(crate) use stream_io::*; +pub(crate) use text_encoding::*; diff --git a/crates/apis/src/random/mod.rs b/crates/javy/src/apis/random/mod.rs similarity index 56% rename from crates/apis/src/random/mod.rs rename to crates/javy/src/apis/random/mod.rs index 89699cea..f27bd7ab 100644 --- a/crates/apis/src/random/mod.rs +++ b/crates/javy/src/apis/random/mod.rs @@ -1,40 +1,33 @@ +use crate::quickjs::{context::Intrinsic, prelude::Func, qjs, Ctx, Object}; 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<()> { - 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))?; +impl Intrinsic for Random { + unsafe fn add_intrinsic(ctx: std::ptr::NonNull) { + register(Ctx::from_raw(ctx)).expect("`Random` APIs to succeed") + } +} - Ok::<_, Error>(()) - })?; +fn register(cx: Ctx) -> Result<()> { + let globals = cx.globals(); + let math: Object<'_> = globals.get("Math").expect("Math global to be defined"); + math.set("random", Func::from(fastrand::f64))?; - Ok(()) - } + Ok::<_, Error>(()) } #[cfg(test)] mod tests { - use crate::{random::Random, APIConfig, JSApiSet}; - use anyhow::{Error, Result}; - use javy::{ + use crate::{ quickjs::{context::EvalOptions, Value}, Runtime, }; + use anyhow::{Error, Result}; #[test] fn test_random() -> Result<()> { let runtime = Runtime::default(); - Random.register(&runtime, &APIConfig::default())?; runtime.context().with(|this| { let mut eval_opts = EvalOptions::default(); eval_opts.strict = false; diff --git a/crates/apis/src/stream_io/io.js b/crates/javy/src/apis/stream_io/io.js similarity index 100% rename from crates/apis/src/stream_io/io.js rename to crates/javy/src/apis/stream_io/io.js diff --git a/crates/apis/src/stream_io/mod.rs b/crates/javy/src/apis/stream_io/mod.rs similarity index 75% rename from crates/apis/src/stream_io/mod.rs rename to crates/javy/src/apis/stream_io/mod.rs index 7496e150..a02c8205 100644 --- a/crates/apis/src/stream_io/mod.rs +++ b/crates/javy/src/apis/stream_io/mod.rs @@ -1,15 +1,45 @@ use anyhow::{anyhow, bail, Error, Result}; use std::io::{Read, Stdin, Write}; -use javy::{ +use crate::{ hold, hold_and_release, - quickjs::{qjs::JS_GetArrayBuffer, Function, Object, Value}, - to_js_error, Args, Runtime, + quickjs::{context::Intrinsic, qjs, qjs::JS_GetArrayBuffer, Ctx, Function, Object, Value}, + to_js_error, Args, }; -use crate::{APIConfig, JSApiSet}; +pub struct StreamIO; -pub(super) struct StreamIO; +impl Intrinsic for StreamIO { + unsafe fn add_intrinsic(ctx: std::ptr::NonNull) { + register(Ctx::from_raw(ctx)).expect("Registering StreamIO functions to succeed"); + } +} + +fn register(this: Ctx<'_>) -> Result<()> { + let globals = this.globals(); + if globals.get::<_, Object>("Javy").is_err() { + globals.set("Javy", Object::new(this.clone())?)? + } + + globals.set( + "__javy_io_writeSync", + Function::new(this.clone(), |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + write(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) + }), + )?; + + globals.set( + "__javy_io_readSync", + Function::new(this.clone(), |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + read(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) + }), + )?; + + this.eval(include_str!("io.js"))?; + Ok::<_, Error>(()) +} fn extract_args<'a, 'js: 'a>( args: &'a [Value<'js>], @@ -130,35 +160,3 @@ fn read(args: Args<'_>) -> Result> { Ok(Value::new_number(cx, n as f64)) } - -impl JSApiSet for StreamIO { - fn register<'js>(&self, runtime: &Runtime, _config: &APIConfig) -> Result<()> { - runtime.context().with(|this| { - let globals = this.globals(); - if globals.get::<_, Object>("Javy").is_err() { - globals.set("Javy", Object::new(this.clone())?)? - } - - globals.set( - "__javy_io_writeSync", - Function::new(this.clone(), |cx, args| { - let (cx, args) = hold_and_release!(cx, args); - write(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) - }), - )?; - - globals.set( - "__javy_io_readSync", - Function::new(this.clone(), |cx, args| { - let (cx, args) = hold_and_release!(cx, args); - read(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) - }), - )?; - - this.eval(include_str!("io.js"))?; - Ok::<_, Error>(()) - })?; - - Ok(()) - } -} diff --git a/crates/apis/src/text_encoding/mod.rs b/crates/javy/src/apis/text_encoding/mod.rs similarity index 67% rename from crates/apis/src/text_encoding/mod.rs rename to crates/javy/src/apis/text_encoding/mod.rs index c7b8528a..6772937d 100644 --- a/crates/apis/src/text_encoding/mod.rs +++ b/crates/javy/src/apis/text_encoding/mod.rs @@ -1,44 +1,46 @@ use std::str; -use crate::{APIConfig, JSApiSet}; -use anyhow::{anyhow, bail, Error, Result}; -use javy::{ +use crate::{ hold, hold_and_release, - quickjs::{context::EvalOptions, Exception, Function, String as JSString, TypedArray, Value}, - to_js_error, to_string_lossy, Args, Runtime, + quickjs::{ + context::{EvalOptions, Intrinsic}, + qjs, Ctx, Exception, Function, String as JSString, TypedArray, Value, + }, + to_js_error, to_string_lossy, Args, }; +use anyhow::{anyhow, bail, Error, Result}; -pub(super) struct TextEncoding; +pub struct TextEncoding; -impl JSApiSet for TextEncoding { - fn register<'js>(&self, runtime: &Runtime, _: &APIConfig) -> Result<()> { - runtime.context().with(|this| { - let globals = this.globals(); - globals.set( - "__javy_decodeUtf8BufferToString", - Function::new(this.clone(), |cx, args| { - let (cx, args) = hold_and_release!(cx, args); - decode(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) - }), - )?; - globals.set( - "__javy_encodeStringToUtf8Buffer", - Function::new(this.clone(), |cx, args| { - let (cx, args) = hold_and_release!(cx, args); - encode(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) - }), - )?; - let mut opts = EvalOptions::default(); - opts.strict = false; - this.eval_with_options(include_str!("./text-encoding.js"), opts)?; - - Ok::<_, Error>(()) - })?; - - Ok(()) +impl Intrinsic for TextEncoding { + unsafe fn add_intrinsic(ctx: std::ptr::NonNull) { + register(Ctx::from_raw(ctx)).expect("Register TextEncoding APIs to succeed"); } } +fn register(this: Ctx<'_>) -> Result<()> { + let globals = this.globals(); + globals.set( + "__javy_decodeUtf8BufferToString", + Function::new(this.clone(), |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + decode(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) + }), + )?; + globals.set( + "__javy_encodeStringToUtf8Buffer", + Function::new(this.clone(), |cx, args| { + let (cx, args) = hold_and_release!(cx, args); + encode(hold!(cx.clone(), args)).map_err(|e| to_js_error(cx, e)) + }), + )?; + let mut opts = EvalOptions::default(); + opts.strict = false; + this.eval_with_options(include_str!("./text-encoding.js"), opts)?; + + Ok::<_, Error>(()) +} + /// Decode a UTF-8 byte buffer as a JavaScript String. fn decode(args: Args<'_>) -> Result> { let (cx, args) = args.release(); @@ -119,16 +121,14 @@ fn encode(args: Args<'_>) -> Result> { #[cfg(test)] mod tests { - use crate::{APIConfig, JSApiSet}; + use crate::{quickjs::Value, Config, Runtime}; use anyhow::{Error, Result}; - use javy::{quickjs::Value, Runtime}; - - use super::TextEncoding; #[test] fn test_text_encoder_decoder() -> Result<()> { - let runtime = Runtime::default(); - TextEncoding.register(&runtime, &APIConfig::default())?; + let mut config = Config::default(); + config.text_encoding(true); + let runtime = Runtime::new(config)?; runtime.context().with(|this| { let result: Value<'_> = this.eval( diff --git a/crates/apis/src/text_encoding/text-encoding.js b/crates/javy/src/apis/text_encoding/text-encoding.js similarity index 100% rename from crates/apis/src/text_encoding/text-encoding.js rename to crates/javy/src/apis/text_encoding/text-encoding.js diff --git a/crates/javy/src/config.rs b/crates/javy/src/config.rs index 49d7b856..a04c0413 100644 --- a/crates/javy/src/config.rs +++ b/crates/javy/src/config.rs @@ -1,10 +1,170 @@ +use bitflags::bitflags; + +bitflags! { + /// Flags to represent available JavaScript features. + pub(crate) struct JSIntrinsics: u32 { + const DATE = 1; + const EVAL = 1 << 1; + const REGEXP_COMPILER = 1 << 2; + const REGEXP = 1 << 3; + const JSON = 1 << 4; + const PROXY = 1 << 5; + const MAP_SET = 1 << 6; + const TYPED_ARRAY = 1 << 7; + const PROMISE = 1 << 8; + const BIG_INT = 1 << 9; + const BIG_FLOAT = 1 << 10; + const BIG_DECIMAL = 1 << 11; + const OPERATORS = 1 << 12; + const BIGNUM_EXTENSION = 1 << 13; + const TEXT_ENCODING = 1 << 14; + } +} + +bitflags! { + /// Flags representing implementation of JavaScript intrinsics + /// made available through the `Javy` global. + /// The APIs in this list can be thought of as APIs similar to the ones + /// exposed by Node or Deno. + /// + /// NB: These APIs are meant to be migrated to a runtime-agnostic namespace, + /// once efforts like WinterCG can be adopted. + /// + /// In the near future, Javy will include an extension mechanism, allowing + /// users to extend the runtime with non-standard functionality directly + /// from the CLI, at this point many, if not most, of these APIs will be + /// moved out. + pub(crate) struct JavyIntrinsics: u32 { + const STREAM_IO = 1; + } +} + /// A configuration for [`Runtime`](crate::Runtime). -#[derive(Debug)] -pub struct Config {} +/// +/// These are the global configuration options to create a [`Runtime`](crate::Runtime), +/// and customize its behavior. +pub struct Config { + /// JavaScript features. + pub(crate) intrinsics: JSIntrinsics, + /// Intrinsics exposed through the `Javy` namespace. + pub(crate) javy_intrinsics: JavyIntrinsics, + /// Whether to use a custom console implementation provided by Javy, + /// that redirects stdout to stderr. + pub(crate) redirect_stdout_to_stderr: bool, +} impl Default for Config { /// Creates a [`Config`] with default values. fn default() -> Self { - Self {} + let mut intrinsics = JSIntrinsics::all(); + intrinsics.set(JSIntrinsics::TEXT_ENCODING, false); + Self { + intrinsics, + javy_intrinsics: JavyIntrinsics::empty(), + redirect_stdout_to_stderr: false, + } + } +} + +impl Config { + /// Configures whether the JavaScript `Date` intrinsic will be available. + pub fn date(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::DATE, enable); + self + } + + /// Configures whether the `Eval` intrinsic will be available. + pub fn eval(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::EVAL, enable); + self + } + + /// Configures whether the regular expression compiler will be available. + pub fn regexp_compiler(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::REGEXP_COMPILER, enable); + self + } + + /// Configures whether the `RegExp` intrinsic will be available. + pub fn regexp(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::REGEXP, enable); + self + } + + /// Configures whether the QuickJS native JSON intrinsic will be + /// available. + pub fn json(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::JSON, enable); + self + } + + /// Configures whether proxy object creation will be available. + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy + pub fn proxy(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::PROXY, enable); + self + } + + /// Configures whether the `MapSet` intrinsic will be available. + pub fn map_set(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::MAP_SET, enable); + self + } + + /// Configures whether the `Promise` instrinsic will be available. + pub fn promise(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::PROMISE, enable); + self + } + + /// Configures whether supoort for `BigInt` will be available. + pub fn big_int(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::BIG_INT, enable); + self + } + + /// Configures whether support for `BigFloat` will be available. + pub fn big_float(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::BIG_FLOAT, enable); + self + } + + /// Configures whether supporr for `BigDecimal` will be available. + pub fn big_decimal(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::BIG_DECIMAL, enable); + self + } + + /// Configures whether operator overloading wil be supported. + pub fn operator_overloading(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::OPERATORS, enable); + self + } + + /// Configures whether extensions to `BigNum` will be available. + pub fn bignum_extension(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::BIGNUM_EXTENSION, enable); + self + } + + /// Configures whether the `TextEncoding` and `TextDecoding` intrinsics will + /// be available. NB: This is partial implementation. + pub fn text_encoding(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::TEXT_ENCODING, enable); + self + } + + /// Whether the `Javy.IO` intrinsic will be available. + /// Disabled by default. + pub fn javy_stream_io(&mut self, enable: bool) -> &mut Self { + self.javy_intrinsics.set(JavyIntrinsics::STREAM_IO, enable); + self + } + + /// Enables whether the output of console.log will be redirected to + /// `stderr`. + pub fn redirect_stdout_to_stderr(&mut self, enable: bool) -> &mut Self { + self.redirect_stdout_to_stderr = enable; + self } } diff --git a/crates/javy/src/lib.rs b/crates/javy/src/lib.rs index 3a4bbbea..bb8984bb 100644 --- a/crates/javy/src/lib.rs +++ b/crates/javy/src/lib.rs @@ -40,7 +40,7 @@ //! * `messagepack` - functions for converting between [`quickjs::JSValueRef`] //! and MessagePack byte slices -pub use config::Config; +pub use config::*; pub use rquickjs as quickjs; pub use runtime::Runtime; use std::str; @@ -59,6 +59,8 @@ pub mod messagepack; #[cfg(feature = "json")] pub mod json; +mod apis; + /// A struct to hold the current [`Ctx`] and [`Value`]s passed as arguments to Rust /// functions. /// A struct here is used to explicitly tie these values with a particular @@ -79,7 +81,7 @@ impl<'js> Args<'js> { } } -/// Alias for `Args::hold(cx, args).release()` +/// Alias for [`Args::hold(cx, args).release()`] #[macro_export] macro_rules! hold_and_release { ($cx:expr, $args:expr) => { @@ -87,7 +89,7 @@ macro_rules! hold_and_release { }; } -/// Alias for [Args::hold] +/// Alias for [`Args::hold`] #[macro_export] macro_rules! hold { ($cx:expr, $args:expr) => { diff --git a/crates/javy/src/runtime.rs b/crates/javy/src/runtime.rs index 0355501d..7ccc0491 100644 --- a/crates/javy/src/runtime.rs +++ b/crates/javy/src/runtime.rs @@ -1,11 +1,17 @@ // use crate::quickjs::JSContextRef; use super::from_js_error; +use crate::{ + apis::{Console, NonStandardConsole, Random, StreamIO, TextEncoding}, + config::{JSIntrinsics, JavyIntrinsics}, + Config, +}; use anyhow::{bail, Result}; -use rquickjs::{Context, Module, Runtime as QRuntime}; +use rquickjs::{ + context::{intrinsic, Intrinsic}, + Context, Module, Runtime as QRuntime, +}; use std::mem::ManuallyDrop; -use crate::Config; - /// A JavaScript Runtime. /// /// Javy's [`Runtime`] holds a [`rquickjs::Runtime`] and [`rquickjs::Context`], @@ -31,15 +37,99 @@ pub struct Runtime { impl Runtime { /// Creates a new [Runtime]. - pub fn new(_config: Config) -> Result { + pub fn new(config: Config) -> Result { let rt = ManuallyDrop::new(QRuntime::new()?); // See comment above about configuring GC behaviour. rt.set_gc_threshold(usize::MAX); - let context = ManuallyDrop::new(Context::full(&rt)?); + let context = Self::build_from_config(&rt, &config)?; Ok(Self { inner: rt, context }) } + fn build_from_config(rt: &QRuntime, cfg: &Config) -> Result> { + let intrinsics = &cfg.intrinsics; + let javy_intrinsics = &cfg.javy_intrinsics; + // We always set Random given that the principles around snapshotting and + // random are applicable when using Javy from the CLI (the usage of + // Wizer from the CLI is not optional). + // NB: Users of Javy as a crate are welcome to switch this config, + // however note that the usage of a custom `Random` implementation + // should not affect the output of `Math.random()`. + let context = Context::custom::(rt)?; + + // We use `Context::with` to ensure that there's a proper lock on the + // context, making it totally safe to add the intrinsics below. + context.with(|ctx| { + if intrinsics.contains(JSIntrinsics::DATE) { + unsafe { intrinsic::Date::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::EVAL) { + unsafe { intrinsic::Eval::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::REGEXP_COMPILER) { + unsafe { intrinsic::RegExpCompiler::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::REGEXP) { + unsafe { intrinsic::RegExp::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::JSON) { + unsafe { intrinsic::Json::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::PROXY) { + unsafe { intrinsic::Proxy::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::MAP_SET) { + unsafe { intrinsic::MapSet::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::TYPED_ARRAY) { + unsafe { intrinsic::TypedArrays::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::PROMISE) { + unsafe { intrinsic::Promise::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::BIG_INT) { + unsafe { intrinsic::BigInt::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::BIG_FLOAT) { + unsafe { intrinsic::BigFloat::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::BIG_DECIMAL) { + unsafe { intrinsic::BigDecimal::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::BIGNUM_EXTENSION) { + unsafe { intrinsic::BignumExt::add_intrinsic(ctx.as_raw()) } + } + + if intrinsics.contains(JSIntrinsics::TEXT_ENCODING) { + unsafe { TextEncoding::add_intrinsic(ctx.as_raw()) } + } + + if cfg.redirect_stdout_to_stderr { + unsafe { NonStandardConsole::add_intrinsic(ctx.as_raw()) } + } else { + unsafe { Console::add_intrinsic(ctx.as_raw()) } + } + + if javy_intrinsics.contains(JavyIntrinsics::STREAM_IO) { + unsafe { StreamIO::add_intrinsic(ctx.as_raw()) } + } + }); + + Ok(ManuallyDrop::new(context)) + } + /// A reference to the inner [Context]. pub fn context(&self) -> &Context { &self.context