diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a0e8759..d037fe47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,10 +33,7 @@ jobs: - name: Test env: CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime --dir=. - run: cargo hack test --target=wasm32-wasip1 --workspace --exclude=javy-cli --exclude=javy-config --exclude=javy-runner --each-feature -- --nocapture - - - name: Test Config - run: cargo test --package=javy-config + run: cargo hack test --target=wasm32-wasip1 --workspace --exclude=javy-cli --exclude=javy-runner --each-feature -- --nocapture - name: Test Runner run: cargo test --package=javy-runner diff --git a/Cargo.lock b/Cargo.lock index dc8f72a8..cd6d19b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1511,7 +1511,7 @@ dependencies = [ [[package]] name = "javy" -version = "3.0.2-alpha.1" +version = "3.1.0-alpha.1" dependencies = [ "anyhow", "bitflags", @@ -1537,7 +1537,6 @@ dependencies = [ "clap", "convert_case", "criterion", - "javy-config", "javy-runner", "javy-test-macros", "lazy_static", @@ -1558,20 +1557,12 @@ dependencies = [ "wizer", ] -[[package]] -name = "javy-config" -version = "3.1.2" -dependencies = [ - "bitflags", -] - [[package]] name = "javy-core" version = "0.2.0" dependencies = [ "anyhow", "javy", - "javy-config", "once_cell", ] diff --git a/Cargo.toml b/Cargo.toml index c6ef7ce3..99db0843 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "crates/core", "crates/cli", "crates/test-macros", - "crates/config", "crates/runner", "fuzz", ] @@ -23,11 +22,11 @@ wasmtime-wasi = "19" wasi-common = "19" anyhow = "1.0" once_cell = "1.20" -bitflags = "2.6.0" -javy-config = { path = "crates/config" } -javy = { path = "crates/javy", version = "3.0.2-alpha.1" } +javy = { path = "crates/javy", version = "3.1.0-alpha.1" } tempfile = "3.13.0" uuid = { version = "1.10", features = ["v4"] } +serde = { version = "1.0", default-features = false } +serde_json = "1.0" [profile.release] lto = true diff --git a/Makefile b/Makefile index 001e2ff1..228ef128 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ docs: cargo doc --package=javy-core --open --target=wasm32-wasip1 test-javy: - CARGO_TARGET_WASM32_WASIP1_RUNNER="wasmtime --dir=." cargo test --package=javy --target=wasm32-wasip1 --features json,messagepack -- --nocapture + CARGO_TARGET_WASM32_WASIP1_RUNNER="wasmtime --dir=." cargo hack test --package=javy --target=wasm32-wasip1 --each-feature -- --nocapture test-core: CARGO_TARGET_WASM32_WASIP1_RUNNER="wasmtime" cargo test --package=javy-core --target=wasm32-wasip1 -- --nocapture @@ -44,10 +44,7 @@ test-wpt: npm install --prefix wpt npm test --prefix wpt -test-config: - CARGO_PROFILE_RELEASE_LTO=off cargo test --package=javy-config -- --nocapture - -tests: test-javy test-core test-runner test-cli test-wpt test-config +tests: test-javy test-core test-runner test-cli test-wpt fmt: fmt-javy fmt-core fmt-cli diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 18f3deb6..7ac48250 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -33,12 +33,11 @@ convert_case = "0.6.0" wasm-opt = "0.116.1" tempfile = { workspace = true } clap = { version = "4.5.19", features = ["derive"] } -javy-config = { workspace = true } +serde = { workspace = true, default-features = false } +serde_json = { workspace = true } [dev-dependencies] -serde_json = "1.0" lazy_static = "1.5" -serde = { version = "1.0", default-features = false, features = ["derive"] } criterion = "0.5" num-format = "0.4.4" wasmparser = "0.218.0" diff --git a/crates/cli/src/codegen/builder.rs b/crates/cli/src/codegen/builder.rs index 7e5dc14d..7df7414d 100644 --- a/crates/cli/src/codegen/builder.rs +++ b/crates/cli/src/codegen/builder.rs @@ -1,9 +1,9 @@ use crate::{ codegen::{CodeGenType, Generator}, + js_config::JsConfig, providers::Provider, }; use anyhow::{bail, Result}; -use javy_config::Config; use std::path::PathBuf; /// Options for using WIT in the code generation process. @@ -83,9 +83,9 @@ impl CodeGenBuilder { } /// Build a [`CodeGenerator`]. - pub fn build(self, ty: CodeGenType, js_runtime_config: Config) -> Result { + pub fn build(self, ty: CodeGenType, js_runtime_config: JsConfig) -> Result { if let CodeGenType::Dynamic = ty { - if js_runtime_config != Config::default() { + if js_runtime_config.has_configs() { bail!("Cannot set JS runtime options when building a dynamic module") } } diff --git a/crates/cli/src/codegen/mod.rs b/crates/cli/src/codegen/mod.rs index 5f309051..64695cfe 100644 --- a/crates/cli/src/codegen/mod.rs +++ b/crates/cli/src/codegen/mod.rs @@ -38,8 +38,6 @@ use std::{fs, rc::Rc, sync::OnceLock}; pub(crate) use builder::*; -use javy_config::Config; - mod transform; mod exports; @@ -52,7 +50,7 @@ use wasi_common::{pipe::ReadPipe, sync::WasiCtxBuilder, WasiCtx}; use wasm_opt::{OptimizationOptions, ShrinkLevel}; use wizer::{Linker, Wizer}; -use crate::{providers::Provider, JS}; +use crate::{js_config::JsConfig, providers::Provider, JS}; use anyhow::Result; static mut WASI: OnceLock = OnceLock::new(); @@ -112,7 +110,7 @@ pub(crate) struct Generator { /// Codegen type to use. pub ty: CodeGenType, /// JS runtime config. - pub js_runtime_config: Config, + pub js_runtime_config: JsConfig, /// Provider to use. pub provider: Provider, /// JavaScript function exports. @@ -125,7 +123,7 @@ pub(crate) struct Generator { impl Generator { /// Creates a new [`Generator`]. - pub fn new(ty: CodeGenType, js_runtime_config: Config, provider: Provider) -> Self { + pub fn new(ty: CodeGenType, js_runtime_config: JsConfig, provider: Provider) -> Self { Self { ty, js_runtime_config, @@ -141,15 +139,14 @@ impl Generator { let config = transform::module_config(); let module = match &self.ty { CodeGenType::Static => { - // Copy config bits into stdin for `initialize_runtime` function. + // Copy config JSON into stdin for `initialize_runtime` function. + let runtime_config = self.js_runtime_config.to_json()?; unsafe { WASI.get_or_init(|| { WasiCtxBuilder::new() .inherit_stderr() .inherit_stdout() - .stdin(Box::new(ReadPipe::from( - self.js_runtime_config.bits().to_le_bytes().as_slice(), - ))) + .stdin(Box::new(ReadPipe::from(runtime_config))) .build() }); }; @@ -463,21 +460,21 @@ fn print_wat(_wasm_binary: &[u8]) -> Result<()> { #[cfg(test)] mod test { + use crate::js_config::JsConfig; use crate::providers::Provider; use super::Generator; use super::WitOptions; use anyhow::Result; - use javy_config::Config; #[test] fn default_values() -> Result<()> { let gen = Generator::new( crate::codegen::CodeGenType::Dynamic, - Config::default(), + JsConfig::default(), Provider::Default, ); - assert_eq!(gen.js_runtime_config, Config::default()); + assert!(!gen.js_runtime_config.has_configs()); assert!(gen.source_compression); assert!(matches!(gen.provider, Provider::Default)); assert_eq!(gen.wit_opts, WitOptions::default()); diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index c1e57a57..5249563e 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -1,11 +1,14 @@ -use crate::option_group; +use crate::{js_config::JsConfig, option::OptionMeta, option_group, providers::Provider}; use anyhow::{anyhow, Result}; use clap::{ builder::{StringValueParser, TypedValueParser, ValueParserFactory}, - Parser, Subcommand, + error::ErrorKind, + CommandFactory, Parser, Subcommand, +}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, }; -use javy_config::Config; -use std::path::PathBuf; use crate::codegen::WitOptions; use crate::option::{ @@ -77,6 +80,9 @@ pub struct CompileCommandOpts { pub no_source_compression: bool, } +const RUNTIME_CONFIG_ARG_SHORT: char = 'J'; +const RUNTIME_CONFIG_ARG_LONG: &str = "javascript"; + #[derive(Debug, Parser)] pub struct BuildCommandOpts { #[arg(value_name = "INPUT", required = true)] @@ -92,10 +98,10 @@ pub struct BuildCommandOpts { /// Use `-C help` for more details. pub codegen: Vec>, - #[arg(short = 'J', long = "javascript")] + #[arg(short = RUNTIME_CONFIG_ARG_SHORT, long = RUNTIME_CONFIG_ARG_LONG)] /// JavaScript runtime options. /// Use `-J help` for more details. - pub js: Vec>, + pub js: Vec, } #[derive(Debug, Parser)] @@ -212,163 +218,255 @@ impl TryFrom>> for CodegenOptionGroup { } } -/// JavaScript option group. -/// This group gets configured from the [`JsOption`] enum. -// -// NB: The documentation for each field is ommitted given that it's similar to -// the enum used to configured the group. -#[derive(Clone, Debug, PartialEq)] -pub struct JsOptionGroup { - pub redirect_stdout_to_stderr: bool, - pub javy_json: bool, - pub simd_json_builtins: bool, - pub javy_stream_io: bool, - pub text_encoding: bool, +/// A runtime config group value. +#[derive(Debug, Clone)] +pub(super) enum JsGroupValue { + Option(JsGroupOption), + Help, } -impl Default for JsOptionGroup { - fn default() -> Self { - Config::default().into() - } +/// A runtime config group option. +#[derive(Debug, Clone)] +pub(super) struct JsGroupOption { + /// The property name used for the option. + name: String, + /// Whether the config is enabled or not. + enabled: bool, } -option_group! { - #[derive(Clone, Debug)] - pub enum JsOption { - /// Whether to redirect the output of console.log to standard error. - RedirectStdoutToStderr(bool), - /// Whether to enable the `Javy.JSON` builtins. - JavyJson(bool), - /// Whether to enable the `Javy.readSync` and `Javy.writeSync` builtins. - JavyStreamIo(bool), - /// Whether to override the `JSON.parse` and `JSON.stringify` - /// implementations with an alternative, more performant, SIMD based - /// implemetation. - SimdJsonBuiltins(bool), - /// Whether to enable support for the `TextEncoder` and `TextDecoder` - /// APIs. - TextEncoding(bool), +#[derive(Debug, Clone)] +pub(super) struct JsGroupOptionParser; + +impl ValueParserFactory for JsGroupValue { + type Parser = JsGroupOptionParser; + + fn value_parser() -> Self::Parser { + JsGroupOptionParser } } -impl From>> for JsOptionGroup { - fn from(value: Vec>) -> Self { - let mut group = Self::default(); +impl TypedValueParser for JsGroupOptionParser { + type Value = JsGroupValue; - for option in value.iter().flat_map(|e| e.0.iter()) { - match option { - JsOption::RedirectStdoutToStderr(enabled) => { - group.redirect_stdout_to_stderr = *enabled; - } - JsOption::JavyJson(enable) => group.javy_json = *enable, - JsOption::SimdJsonBuiltins(enable) => group.simd_json_builtins = *enable, - JsOption::TextEncoding(enable) => group.text_encoding = *enable, - JsOption::JavyStreamIo(enable) => group.javy_stream_io = *enable, - } + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> std::result::Result { + let val = StringValueParser::new().parse_ref(cmd, arg, value)?; + + if val == "help" { + return Ok(JsGroupValue::Help); } - group + let mut splits = val.splitn(2, '='); + let key = splits.next().unwrap(); + let value = match splits.next() { + Some("y") => true, + Some("n") => false, + None => true, + _ => return Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)), + }; + Ok(JsGroupValue::Option(JsGroupOption { + name: key.to_string(), + enabled: value, + })) } } -impl From for Config { - fn from(value: JsOptionGroup) -> Self { - let mut config = Self::default(); - config.set( - Config::REDIRECT_STDOUT_TO_STDERR, - value.redirect_stdout_to_stderr, - ); - config.set(Config::JAVY_JSON, value.javy_json); - config.set(Config::SIMD_JSON_BUILTINS, value.simd_json_builtins); - config.set(Config::JAVY_STREAM_IO, value.javy_stream_io); - config.set(Config::TEXT_ENCODING, value.text_encoding); - config - } -} +impl JsConfig { + /// Build a JS runtime config from valid runtime config values. + pub(super) fn from_group_values( + provider: &Provider, + group_values: Vec, + ) -> Result { + let supported_properties = provider.config_schema()?; + + let mut supported_names = HashSet::new(); + for property in &supported_properties { + supported_names.insert(property.name.as_str()); + } -impl From for JsOptionGroup { - fn from(value: Config) -> Self { - Self { - redirect_stdout_to_stderr: value.contains(Config::REDIRECT_STDOUT_TO_STDERR), - javy_json: value.contains(Config::JAVY_JSON), - simd_json_builtins: value.contains(Config::SIMD_JSON_BUILTINS), - javy_stream_io: value.contains(Config::JAVY_STREAM_IO), - text_encoding: value.contains(Config::TEXT_ENCODING), + let mut config = HashMap::new(); + for value in group_values { + match value { + JsGroupValue::Help => { + fmt_help( + RUNTIME_CONFIG_ARG_LONG, + &RUNTIME_CONFIG_ARG_SHORT.to_string(), + &supported_properties + .into_iter() + .map(|prop| OptionMeta { + name: prop.name, + help: "[=y|n]".to_string(), + doc: prop.doc, + }) + .collect::>(), + ); + std::process::exit(0); + } + JsGroupValue::Option(JsGroupOption { name, enabled }) => { + if supported_names.contains(name.as_str()) { + config.insert(name, enabled); + } else { + Cli::command() + .error( + ErrorKind::InvalidValue, + format!( + "Property {name} is not supported for runtime configuration", + ), + ) + .exit(); + } + } + } } + Ok(JsConfig::from_hash(config)) } } #[cfg(test)] mod tests { - use super::{CodegenOption, CodegenOptionGroup, GroupOption, JsOption, JsOptionGroup}; + use crate::{ + commands::{JsGroupOption, JsGroupValue}, + js_config::JsConfig, + providers::Provider, + }; + + use super::{CodegenOption, CodegenOptionGroup, GroupOption}; use anyhow::Result; - use javy_config::Config; #[test] - fn js_group_conversion_between_vector_of_options_and_group() -> Result<()> { - let group: JsOptionGroup = vec![].into(); - - assert_eq!(group, JsOptionGroup::default()); - - let raw = vec![GroupOption(vec![JsOption::RedirectStdoutToStderr(false)])]; - let group: JsOptionGroup = raw.into(); - let expected = JsOptionGroup { - redirect_stdout_to_stderr: false, - ..Default::default() - }; - - assert_eq!(group, expected); - - let raw = vec![GroupOption(vec![JsOption::JavyJson(false)])]; - let group: JsOptionGroup = raw.into(); - let expected = JsOptionGroup { - javy_json: false, - ..Default::default() - }; - assert_eq!(group, expected); - - let raw = vec![GroupOption(vec![JsOption::JavyStreamIo(false)])]; - let group: JsOptionGroup = raw.into(); - let expected = JsOptionGroup { - javy_stream_io: false, - ..Default::default() - }; - assert_eq!(group, expected); - - let raw = vec![GroupOption(vec![JsOption::SimdJsonBuiltins(false)])]; - let group: JsOptionGroup = raw.into(); - - let expected = JsOptionGroup { - simd_json_builtins: false, - ..Default::default() - }; - assert_eq!(group, expected); - - let raw = vec![GroupOption(vec![JsOption::TextEncoding(false)])]; - let group: JsOptionGroup = raw.into(); - - let expected = JsOptionGroup { - text_encoding: false, - ..Default::default() - }; - assert_eq!(group, expected); - - let raw = vec![GroupOption(vec![ - JsOption::JavyStreamIo(false), - JsOption::JavyJson(false), - JsOption::RedirectStdoutToStderr(false), - JsOption::TextEncoding(false), - JsOption::SimdJsonBuiltins(false), - ])]; - let group: JsOptionGroup = raw.into(); - let expected = JsOptionGroup { - javy_stream_io: false, - javy_json: false, - redirect_stdout_to_stderr: false, - text_encoding: false, - simd_json_builtins: false, - }; - assert_eq!(group, expected); + fn js_config_from_config_values() -> Result<()> { + let group = JsConfig::from_group_values(&Provider::Default, vec![])?; + assert!(!group.has_configs()); + assert_eq!(group.get("redirect-stdout-to-stderr"), None); + assert_eq!(group.get("javy-json"), None); + assert_eq!(group.get("javy-stream-io"), None); + assert_eq!(group.get("simd-json-builtins"), None); + assert_eq!(group.get("text-encoding"), None); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "redirect-stdout-to-stderr".to_string(), + enabled: false, + })], + )?; + assert_eq!(group.get("redirect-stdout-to-stderr"), Some(false)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "redirect-stdout-to-stderr".to_string(), + enabled: true, + })], + )?; + assert_eq!(group.get("redirect-stdout-to-stderr"), Some(true)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "javy-json".to_string(), + enabled: false, + })], + )?; + assert_eq!(group.get("javy-json"), Some(false)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "javy-json".to_string(), + enabled: true, + })], + )?; + assert_eq!(group.get("javy-json"), Some(true)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "javy-stream-io".to_string(), + enabled: false, + })], + )?; + assert_eq!(group.get("javy-stream-io"), Some(false)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "javy-stream-io".to_string(), + enabled: true, + })], + )?; + assert_eq!(group.get("javy-stream-io"), Some(true)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "simd-json-builtins".to_string(), + enabled: false, + })], + )?; + assert_eq!(group.get("simd-json-builtins"), Some(false)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "simd-json-builtins".to_string(), + enabled: true, + })], + )?; + assert_eq!(group.get("simd-json-builtins"), Some(true)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "text-encoding".to_string(), + enabled: false, + })], + )?; + assert_eq!(group.get("text-encoding"), Some(false)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![JsGroupValue::Option(JsGroupOption { + name: "text-encoding".to_string(), + enabled: true, + })], + )?; + assert_eq!(group.get("text-encoding"), Some(true)); + + let group = JsConfig::from_group_values( + &Provider::Default, + vec![ + JsGroupValue::Option(JsGroupOption { + name: "redirect-stdout-to-stderr".to_string(), + enabled: false, + }), + JsGroupValue::Option(JsGroupOption { + name: "javy-json".to_string(), + enabled: false, + }), + JsGroupValue::Option(JsGroupOption { + name: "javy-stream-io".to_string(), + enabled: false, + }), + JsGroupValue::Option(JsGroupOption { + name: "simd-json-builtins".to_string(), + enabled: false, + }), + JsGroupValue::Option(JsGroupOption { + name: "text-encoding".to_string(), + enabled: false, + }), + ], + )?; + assert_eq!(group.get("redirect-stdout-to-stderr"), Some(false)); + assert_eq!(group.get("javy-json"), Some(false)); + assert_eq!(group.get("javy-stream-io"), Some(false)); + assert_eq!(group.get("simd-json-builtins"), Some(false)); + assert_eq!(group.get("text-encoding"), Some(false)); Ok(()) } @@ -398,13 +496,4 @@ mod tests { Ok(()) } - - #[test] - fn js_conversion_between_group_and_config() -> Result<()> { - assert_eq!(JsOptionGroup::default(), Config::default().into()); - - let cfg: Config = JsOptionGroup::default().into(); - assert_eq!(cfg, Config::default()); - Ok(()) - } } diff --git a/crates/cli/src/js_config.rs b/crates/cli/src/js_config.rs new file mode 100644 index 00000000..463f74a1 --- /dev/null +++ b/crates/cli/src/js_config.rs @@ -0,0 +1,29 @@ +use anyhow::Result; +use std::collections::HashMap; + +/// A collection of property names to whether they are enabled. +#[derive(Debug, Default)] +pub(crate) struct JsConfig(HashMap); + +impl JsConfig { + /// Create from a hash. + pub(crate) fn from_hash(configs: HashMap) -> Self { + JsConfig(configs) + } + + /// Returns true if any configs are set. + pub(crate) fn has_configs(&self) -> bool { + !self.0.is_empty() + } + + /// Encode as JSON. + pub(crate) fn to_json(&self) -> Result> { + Ok(serde_json::to_vec(&self.0)?) + } + + #[cfg(test)] + /// Retrieve a value for a property name. + pub(crate) fn get(&self, name: &str) -> Option { + self.0.get(name).copied() + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 350f82e1..f174bca6 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,6 +2,7 @@ mod bytecode; mod codegen; mod commands; mod js; +mod js_config; mod option; mod providers; mod wit; @@ -11,9 +12,9 @@ use crate::commands::{Cli, Command, EmitProviderCommandOpts}; use anyhow::Result; use clap::Parser; use codegen::{CodeGenBuilder, CodeGenType}; -use commands::{CodegenOptionGroup, JsOptionGroup}; -use javy_config::Config; +use commands::CodegenOptionGroup; use js::JS; +use js_config::JsConfig; use providers::Provider; use std::fs; use std::fs::File; @@ -46,7 +47,7 @@ fn main() -> Result<()> { ))?) .source_compression(!opts.no_source_compression); - let config = Config::default(); + let config = JsConfig::default(); let mut gen = if opts.dynamic { builder.provider(Provider::V2); builder.build(CodeGenType::Dynamic, config)? @@ -67,11 +68,11 @@ fn main() -> Result<()> { .wit_opts(codegen.wit) .source_compression(codegen.source_compression); - let js_opts: JsOptionGroup = opts.js.clone().into(); + let js_opts = JsConfig::from_group_values(&Provider::Default, opts.js.clone())?; let mut gen = if codegen.dynamic { - builder.build(CodeGenType::Dynamic, js_opts.into())? + builder.build(CodeGenType::Dynamic, js_opts)? } else { - builder.build(CodeGenType::Static, js_opts.into())? + builder.build(CodeGenType::Static, js_opts)? }; let wasm = gen.generate(&js)?; diff --git a/crates/cli/src/providers.rs b/crates/cli/src/providers.rs index 147063b6..6e4e190f 100644 --- a/crates/cli/src/providers.rs +++ b/crates/cli/src/providers.rs @@ -1,12 +1,28 @@ use crate::bytecode; use anyhow::{anyhow, Result}; -use std::str; +use serde::Deserialize; +use std::{ + io::{Read, Seek}, + str, +}; +use wasi_common::{pipe::WritePipe, sync::WasiCtxBuilder}; +use wasmtime::{AsContextMut, Engine, Linker}; const QUICKJS_PROVIDER_MODULE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/provider.wasm")); /// Use the legacy provider when using the `compile -d` command. const QUICKJS_PROVIDER_V2_MODULE: &[u8] = include_bytes!("./javy_quickjs_provider_v2.wasm"); +/// A property that is in the config schema returned by the provider. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct JsConfigProperty { + /// The name of the property (e.g., `simd-json-builtins`). + pub(crate) name: String, + /// The documentation to display for the property. + pub(crate) doc: String, +} + /// Different providers that are available. #[derive(Debug)] pub enum Provider { @@ -58,4 +74,50 @@ impl Provider { } } } + + /// The JS configuration properties supported by this provider. + pub fn config_schema(&self) -> Result> { + match self { + Self::V2 => Ok(vec![]), + Self::Default => { + let engine = Engine::default(); + let module = wasmtime::Module::new(&engine, self.as_bytes())?; + let mut linker = Linker::new(&engine); + wasi_common::sync::snapshots::preview_1::add_wasi_snapshot_preview1_to_linker( + &mut linker, + |s| s, + )?; + let stdout = WritePipe::new_in_memory(); + let wasi = WasiCtxBuilder::new() + .inherit_stderr() + .stdout(Box::new(stdout.clone())) + .build(); + let mut store = wasmtime::Store::new(&engine, wasi); + let instance = linker.instantiate(store.as_context_mut(), &module)?; + instance + .get_typed_func::<(), ()>(store.as_context_mut(), "config_schema")? + .call(store.as_context_mut(), ())?; + drop(store); + let mut config_json = vec![]; + let mut cursor = stdout.try_into_inner().unwrap(); + cursor.rewind()?; + cursor.read_to_end(&mut config_json)?; + let config_schema = serde_json::from_slice::(&config_json)?; + let mut configs = Vec::with_capacity(config_schema.supported_properties.len()); + for config in config_schema.supported_properties { + configs.push(JsConfigProperty { + name: config.name, + doc: config.doc, + }); + } + Ok(configs) + } + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ConfigSchema { + supported_properties: Vec, } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml deleted file mode 100644 index 15b268f8..00000000 --- a/crates/config/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "javy-config" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -bitflags = { workspace = true } diff --git a/crates/config/README.md b/crates/config/README.md deleted file mode 100644 index 7dfcd62b..00000000 --- a/crates/config/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Shared Configuration for Javy - -See `src/lib.rs` for more details. diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs deleted file mode 100644 index ab816aca..00000000 --- a/crates/config/src/lib.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Shared Configuration for Javy. -//! -//! This crate serves as a central place to facilitate configuration sharing -//! between the Javy CLI and the Javy crate. It addresses the challenge of -//! passing configuration settings in environments where the Javy CLI commands -//! predominantly execute WebAssembly. -//! -//! The purpose of this crate is to consolidate configuration parameters, -//! ensuring consistent and accessible settings across both the CLI and the -//! crate. This approach simplifies the management of configuration settings and -//! enhances the integration between different components of the Javy ecosystem. -//! -//! Currently, this crate includes only a subset of the available configuration -//! options. The objective is to eventually encompass all configurable -//! parameters found in [javy::Config]. -//! -//! The selection of the current configuration options was influenced by the -//! need to override non-standard defaults typically set during CLI invocations. -//! These defaults often do not align with the preferences of the CLI users. -//! -//! In general this crate should be treated as an internal detail and -//! a contract between the CLI and the Javy crate. - -use bitflags::bitflags; - -bitflags! { - #[derive(Eq, PartialEq, Debug)] - pub struct Config: u32 { - const SIMD_JSON_BUILTINS = 1; - const JAVY_JSON = 1 << 1; - const JAVY_STREAM_IO = 1 << 2; - const REDIRECT_STDOUT_TO_STDERR = 1 << 3; - const TEXT_ENCODING = 1 << 4; - } -} - -impl Default for Config { - fn default() -> Self { - let mut config = Config::empty(); - config.set(Config::SIMD_JSON_BUILTINS, true); - config.set(Config::JAVY_JSON, true); - config.set(Config::JAVY_STREAM_IO, true); - config.set(Config::REDIRECT_STDOUT_TO_STDERR, true); - config.set(Config::TEXT_ENCODING, true); - config - } -} - -#[cfg(test)] -mod tests { - use super::Config; - #[test] - fn check_bits() { - assert!(Config::SIMD_JSON_BUILTINS == Config::from_bits(1).unwrap()); - assert!(Config::JAVY_JSON == Config::from_bits(1 << 1).unwrap()); - assert!(Config::JAVY_STREAM_IO == Config::from_bits(1 << 2).unwrap()); - assert!(Config::REDIRECT_STDOUT_TO_STDERR == Config::from_bits(1 << 3).unwrap()); - assert!(Config::TEXT_ENCODING == Config::from_bits(1 << 4).unwrap()); - } -} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 4f53656e..5d6e7d3b 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -11,9 +11,12 @@ crate-type = ["cdylib"] [dependencies] anyhow = { workspace = true } -javy = { workspace = true, features = ["export_alloc_fns", "json"] } +javy = { workspace = true, features = [ + "export_alloc_fns", + "json", + "shared_config", +] } once_cell = { workspace = true } -javy-config = { workspace = true } [features] experimental_event_loop = [] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 6869c949..f2dec986 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,17 +1,15 @@ use anyhow::anyhow; -use javy::Runtime; -use javy_config::Config; +use javy::Config; +use javy::{Runtime, SharedConfig}; use namespace::import_namespace; use once_cell::sync::OnceCell; use std::io; -use std::io::stdin; use std::io::Read; use std::slice; use std::str; mod execution; mod namespace; -mod runtime; const FUNCTION_MODULE_NAME: &str = "function.mjs"; @@ -24,27 +22,38 @@ import_namespace!("javy_quickjs_provider_v3"); /// Used by Wizer to preinitialize the module. #[export_name = "initialize_runtime"] pub extern "C" fn initialize_runtime() { - // Read config bits from stdin. + // Read shared config JSON in from stdin. // Using stdin instead of an environment variable because the value set for // an environment variable will persist as the value set for that environment // variable in subsequent invocations so a different value can't be used to // initialize a runtime with a different configuration. - let mut config_bytes = [0; 4]; - let js_runtime_config = match stdin().read_exact(&mut config_bytes) { - Ok(()) => Config::from_bits(u32::from_le_bytes(config_bytes)) - .expect("stdin should only contain valid config flags"), - // Not having 4 bytes of configuration means the configuration hasn't - // been set so the default configuration should be used. - Err(e) if matches!(e.kind(), io::ErrorKind::UnexpectedEof) => Config::default(), + let mut config = Config::default(); + // Preserve defaults that used to be passed from the Javy CLI. + config + .text_encoding(true) + .redirect_stdout_to_stderr(true) + .javy_stream_io(true) + .simd_json_builtins(true) + .javy_json(true); + + let mut config_bytes = vec![]; + let shared_config = match io::stdin().read_to_end(&mut config_bytes) { + Ok(0) => None, + Ok(_) => Some(SharedConfig::parse_from_json(&config_bytes).unwrap()), Err(e) => panic!("Error reading from stdin: {e}"), }; - let runtime = runtime::new(js_runtime_config).unwrap(); + if let Some(shared_config) = shared_config { + shared_config.apply_to_config(&mut config); + } + + let runtime = Runtime::new(config).unwrap(); unsafe { RUNTIME.take(); // Allow re-initializing. RUNTIME .set(runtime) - // `set` requires `T` to implement `Debug` but quickjs::{Runtime, - // Context} don't. + // `unwrap` requires error `T` to implement `Debug` but `set` + // returns the `javy::Runtime` on error and `javy::Runtime` does not + // implement `Debug`. .map_err(|_| anyhow!("Could not pre-initialize javy::Runtime")) .unwrap(); }; diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs deleted file mode 100644 index b707c732..00000000 --- a/crates/core/src/runtime.rs +++ /dev/null @@ -1,15 +0,0 @@ -use anyhow::Result; -use javy::{Config, Runtime}; -use javy_config::Config as SharedConfig; - -pub(crate) fn new(shared_config: SharedConfig) -> Result { - let mut config = Config::default(); - let config = config - .text_encoding(shared_config.contains(SharedConfig::TEXT_ENCODING)) - .redirect_stdout_to_stderr(shared_config.contains(SharedConfig::REDIRECT_STDOUT_TO_STDERR)) - .javy_stream_io(shared_config.contains(SharedConfig::JAVY_STREAM_IO)) - .simd_json_builtins(shared_config.contains(SharedConfig::SIMD_JSON_BUILTINS)) - .javy_json(shared_config.contains(SharedConfig::JAVY_JSON)); - - Runtime::new(std::mem::take(config)) -} diff --git a/crates/javy/CHANGELOG.md b/crates/javy/CHANGELOG.md index 0ccf12ef..30894e3d 100644 --- a/crates/javy/CHANGELOG.md +++ b/crates/javy/CHANGELOG.md @@ -8,6 +8,12 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- `shared_config` feature so Javy providers can export what runtime + configuration options they support and have a way of interpreting a byte + array containing a configuration. + ## [3.0.1] - 2024-09-18 ### Changed diff --git a/crates/javy/Cargo.toml b/crates/javy/Cargo.toml index 035383d7..7a2deda5 100644 --- a/crates/javy/Cargo.toml +++ b/crates/javy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "javy" -version = "3.0.2-alpha.1" +version = "3.1.0-alpha.1" authors.workspace = true edition.workspace = true license.workspace = true @@ -14,14 +14,14 @@ anyhow = { workspace = true } rquickjs = { version = "=0.6.1", features = ["array-buffer", "bindgen"] } rquickjs-core = "=0.6.1" rquickjs-sys = "=0.6.1" -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0", optional = true } +serde = { workspace = true, default-features = true, features = ["derive"] } +serde_json = { workspace = true, optional = true } serde-transcode = { version = "1.1", optional = true } 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 = { workspace = true } +bitflags = "2.6.0" fastrand = "2.1.1" simd-json = { version = "0.14.0", optional = true, default-features = false, features = [ "big-int-as-float", @@ -33,6 +33,7 @@ javy-test-macros = { path = "../test-macros/" } [features] export_alloc_fns = [] +shared_config = ["serde_json"] messagepack = ["rmp-serde", "serde-transcode"] # According to our benchmarks and experiments, the fastest and most efficient # JSON implementation comes from: diff --git a/crates/javy/src/lib.rs b/crates/javy/src/lib.rs index 63607f27..b30c05fa 100644 --- a/crates/javy/src/lib.rs +++ b/crates/javy/src/lib.rs @@ -35,6 +35,9 @@ //! * `export_alloc_fns` - exports [`alloc::canonical_abi_realloc`] and //! [`alloc::canonical_abi_free`] from generated WebAssembly for allocating //! and freeing memory +//! * `shared_config` - exports a Wasm function describing supported runtime +//! configuration options and exports a data structure and methods for a +//! provider to interpret a configuration byte array. //! * `json` - functions for converting between [`quickjs::JSValueRef`] and JSON //! byte slices //! * `messagepack` - functions for converting between [`quickjs::JSValueRef`] @@ -62,6 +65,11 @@ pub mod messagepack; #[cfg(feature = "json")] pub mod json; +#[cfg(feature = "shared_config")] +mod shared_config; +#[cfg(feature = "shared_config")] +pub use crate::shared_config::SharedConfig; + mod apis; /// A struct to hold the current [`Ctx`] and [`Value`]s passed as arguments to Rust diff --git a/crates/javy/src/shared_config/mod.rs b/crates/javy/src/shared_config/mod.rs new file mode 100644 index 00000000..99b86a72 --- /dev/null +++ b/crates/javy/src/shared_config/mod.rs @@ -0,0 +1,69 @@ +//! APIs and data structures for receiving runtime configuration from the Javy CLI. + +use anyhow::Result; +use serde::Deserialize; +use std::io::{stdout, Write}; + +mod runtime_config; + +use crate::{runtime_config, Config}; + +runtime_config! { + #[derive(Debug, Default, Deserialize)] + #[serde(deny_unknown_fields, rename_all = "kebab-case")] + pub struct SharedConfig { + /// Whether to redirect the output of console.log to standard error. + redirect_stdout_to_stderr: Option, + #[cfg(feature = "json")] + /// Whether to enable the `Javy.JSON` builtins. + javy_json: Option, + /// Whether to enable the `Javy.readSync` and `Javy.writeSync` builtins. + javy_stream_io: Option, + #[cfg(feature = "json")] + /// Whether to override the `JSON.parse` and `JSON.stringify` + /// implementations with an alternative, more performant, SIMD based + /// implemetation. + simd_json_builtins: Option, + /// Whether to enable support for the `TextEncoder` and `TextDecoder` + /// APIs. + text_encoding: Option, + } +} + +impl SharedConfig { + pub fn parse_from_json(config: &[u8]) -> Result { + Ok(serde_json::from_slice::(config)?) + } + + pub fn apply_to_config(&self, config: &mut Config) { + if let Some(enable) = self.redirect_stdout_to_stderr { + config.redirect_stdout_to_stderr(enable); + } + #[cfg(feature = "json")] + if let Some(enable) = self.javy_json { + config.javy_json(enable); + } + if let Some(enable) = self.javy_stream_io { + config.javy_stream_io(enable); + } + #[cfg(feature = "json")] + if let Some(enable) = self.simd_json_builtins { + config.simd_json_builtins(enable); + } + if let Some(enable) = self.text_encoding { + config.text_encoding(enable); + } + } +} + +#[export_name = "config_schema"] +pub fn config_schema() { + stdout() + .write_all( + serde_json::to_string(&SharedConfig::config_schema()) + .unwrap() + .as_bytes(), + ) + .unwrap(); + stdout().flush().unwrap(); +} diff --git a/crates/javy/src/shared_config/runtime_config.rs b/crates/javy/src/shared_config/runtime_config.rs new file mode 100644 index 00000000..a32ddd74 --- /dev/null +++ b/crates/javy/src/shared_config/runtime_config.rs @@ -0,0 +1,62 @@ +use serde::Serialize; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct ConfigSchema { + pub(super) supported_properties: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct ConfigProperty { + pub(super) name: String, + pub(super) doc: String, +} + +#[macro_export] +macro_rules! runtime_config { + ( + $(#[$attr:meta])* + pub struct $opts:ident { + $( + $( + #[cfg($cfg:meta)] + )? + $( + #[doc = $doc:tt] + )* + $opt:ident: Option, + )+ + } + ) => { + $(#[$attr])* + pub struct $opts { + $( + $( + #[cfg($cfg)] + )? + $( + #[doc = $doc] + )* + $opt: Option, + )+ + } + + impl $opts { + fn config_schema() -> $crate::shared_config::runtime_config::ConfigSchema { + $crate::shared_config::runtime_config::ConfigSchema { + supported_properties: vec![ + $( + { + $crate::shared_config::runtime_config::ConfigProperty { + name: stringify!($opt).replace('_', "-").to_string(), + doc: concat!($($doc, "\n",)*).into(), + } + }, + )+ + ] + } + } + } + } +} diff --git a/docs/docs-contributing-testing-locally.md b/docs/docs-contributing-testing-locally.md index 001a2c56..0a791e5e 100644 --- a/docs/docs-contributing-testing-locally.md +++ b/docs/docs-contributing-testing-locally.md @@ -15,5 +15,5 @@ cargo +stable install cargo-hack --locked 3. Run tests, eg: ``` -CARGO_TARGET_WASM32_WASIP1_RUNNER="wasmtime --dir=." cargo hack test --target=wasm32-wasip1 --workspace --exclude=javy-cli --exclude=javy-config --each-feature -- --nocapture +CARGO_TARGET_WASM32_WASIP1_RUNNER="wasmtime --dir=." cargo hack test --target=wasm32-wasip1 --workspace --exclude=javy-cli --each-feature -- --nocapture ``` diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index b35196ee..75768fd0 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,7 +10,7 @@ cargo-fuzz = true [dependencies] arbitrary-json = "0.1.1" libfuzzer-sys = "0.4" -serde_json = "1.0" +serde_json = { workspace = true } javy = { path = "../crates/javy/", features = ["json"] } anyhow = { workspace = true }