Javy Documentation

repository·main·Indexed 25 days ago

https://github.com/bytecodealliance/javy

A JavaScript to WebAssembly toolchain that enables running JavaScript code within a Wasm environment by embedding a JavaScript runtime. It includes the javy-cli for compiling JavaScript files into Wasm binaries, the javy-codegen crate for module generation, and the javy-plugin-api for creating WASI Preview 2 plugins. Javy utilizes QuickJS via the rquickjs crate and supports both static and dynamic linking to optimize module size.

Tokens
23.8K
Snippets
46
Records
129
Agent score
83%

What's inside Javy

  1. Overview of Javy

    main

    Javy is a JavaScript to WebAssembly toolchain. It allows you to run JavaScript code by executing it within an embedded WebAssembly JavaScript runtime.

    Key characteristics:

    • Small Module Sizes: Using dynamic linking, Javy can create very small Wasm modules in the 1 to 16 KB range.
    • Static Linking: The default static linking produces modules that are at least 869 KB in size.
  2. Understand JavaScript API support in Javy

    main

    Javy supports ES2023 by default. It does not support NodeJS APIs. While Javy aims for WinterCG Common API compatibility, some APIs are currently provided under a custom Javy namespace or have partial support.

    Supported APIs:

    • JSON: Fully supported. Performance can be improved using the -J simd-json-builtins flag.
    • String.prototype.normalize: Fully supported.

    Partially Supported APIs:

    • TextDecoder: Partial support, not fully compliant.
    • TextEncoder: Partial support, not fully compliant.
    • console: Partial support (specifically console.log and console.error).
  3. Understand the Javy Architecture

    main

    Javy is composed of several crates that work together to compile JavaScript to WebAssembly (Wasm) and provide a QuickJS-based runtime. The core workflow involves the javy-cli driving javy-codegen to produce Wasm modules. The runtime itself is powered by the javy crate, which is designed to compile to wasm32-wasip1 or wasm32-wasip2 and provides APIs for configuring a QuickJS environment.

    Core Component Relationships:

    • javy-cli $\rightarrow$ javy-codegen $\rightarrow$ wasm (the resulting module)
    • javy-plugin $\rightarrow$ javy-plugin-api $\rightarrow$ javy $\rightarrow$ rquickjs (the runtime engine)

    Key Crates:

    • javy: The primary library entrypoint for third parties to configure a QuickJS-based runtime.
    • javy-cli: The command-line interface used to compile JS to Wasm.
    • javy-codegen: The Rust crate responsible for the actual JS-to-Wasm compilation.
    • javy-plugin: The default plugin (compiled to plugin.wasm) used by the CLI and dynamic environments. It defines an initialize_runtime function.
    • javy-plugin-api: Provides common implementations for exports (like the invoke function) and custom Wasm sections required by plugins.
  4. Invoke Javy modules programmatically

    main

    Javy-generated modules are designed to be WASI-only and follow the command pattern.

    When invoking Javy modules from a custom embedding (such as a custom WebAssembly runtime), you must handle data via standard I/O:

    • Input: Pass all input data via stdin.
    • Output: Retrieve all output data from stdout.

    If you are using a runtime like Wasmtime, use wasmtime-wasi to set the stdin and retrieve the stdout.

  5. Handle multi-word function exports with kebab-case

    main

    When exporting JavaScript functions with multiple words, you must use kebab-case in the .wit file due to WIT restrictions. Javy automatically maps these kebab-case WIT exports to the corresponding camelCase JavaScript exports. The function is then exported from the Wasm module using the kebab-case name defined in the WIT file.

    // index.js
    export function fooBar() {
      console.log("In foo-bar");
    }
    // index.wit
    package local:main;
    
    world index {
      export foo-bar: func();
    }
    # Build and run using the kebab-case name
    javy build index.js -C wit=index.wit -C wit-world=index -o index.wasm
    wasmtime run --invoke foo-bar index.wasm
  6. Select and manage WPT test suites

    main

    You can control which tests are executed by modifying test_spec.js:

    • Add suites: Add new test suites to test_spec.js.
    • Ignore tests: Include a test's name in its ignore list to skip it.

    If you need to modify an upstream test (e.g., to comment out unsupported functionality like UTF-16 support for TextDecoder), copy the test into the custom_tests directory, apply your changes, and update test_spec.js to point to the local copy instead of the upstream version.

  7. Install build requirements for Javy

    main

    Before building Javy, ensure you have the following dependencies installed based on your operating system:

    Ubuntu Install required system packages using apt-get:

    sudo apt-get install curl pkg-config libssl-dev clang

    NixOS (using flakes)

    1. Install direnv.
    2. Run echo use flake > .envrc in the repository root.
    3. Run direnv allow in the repository root.
    4. Run make or make cli.

    Rust Requirements

    • Install stable Rust via rustup:
      rustup install stable && rustup default stable
    • Add the required WASI targets:
      rustup target add wasm32-wasip1
      rustup target add wasm32-wasip2

    macOS (Apple Silicon)

    • If running on Apple Silicon, install Rosetta 2:
      softwareupdate --install-rosetta
  8. Implement a WASI preview 2 Javy plugin

    main

    WASI preview 2 plugins use Wasm components and require a WIT file to define the interface.

    Steps:

    1. Configure Cargo.toml with javy-plugin-api = "5.0.0" and wit-bindgen = "0.47.0".
    2. Create a wit/world.wit file defining your exports (e.g., compile-src, initialize-runtime, invoke) and any necessary imports.
    3. Use the javy_plugin! macro in src/lib.rs to implement the plugin logic, or implement the Guest trait directly if avoiding the macro.
    4. Build using cargo build --target=wasm32-wasip2 --release.
    5. Initialize the plugin for the Javy CLI using:
      javy init-plugin <path_to_plugin> -o <path_to_initialized_module>

    Note: Because components are converted to modules, parameter and result types are lowered to core Wasm equivalents. Use cabi_realloc for structured data like strings or arrays.

    [package]
    name = "my-plugin-name"
    version = "0.1.0"
    
    [lib]
    name = "my_plugin_name"
    crate-type = ["cdylib"]
    
    [dependencies]
    javy-plugin-api = "5.0.0"
    wit-bindgen = "0.47.0"
    package yournamespace:my-javy-plugin@1.0.0;
    
    world my-javy-plugin {
        export compile-src: func(src: list<u8>) -> result<list<u8>, string>;
        export initialize-runtime: func();
        export invoke: func(bytecode: list<u8>, function: option<string>);
    }
    use javy_plugin_api::{
        javy::{quickjs::prelude::Func, Runtime},
        javy_plugin,
        Config,
    };
    
    wit_bindgen::generate!({ world: "my-javy-plugin", generate_all });
    
    fn config() -> Config {
        Config::default()
    }
    
    fn modify_runtime(runtime: Runtime) -> Runtime {
        runtime.context().with(|ctx| {
            // Creates a `plugin` variable on the global set to `true`.
            ctx.globals().set("plugin", true).unwrap();
            ctx.globals()
                .set(
                    "func",
                    Func::from(|| {
                        crate::imported_function();
                    }),
                )
                .unwrap();
        });
        runtime
    }
    
    struct Component;
    
    // Set your plugin's import namespace.
    javy_plugin!("my-javy-plugin", Component, config, modify_runtime);
    
    export!(Component);
  9. Export JavaScript functions through WebAssembly

    main

    You can export JavaScript functions to the WebAssembly module by providing a .wit file and specifying a wit world when running javy build.

    Constraints:

    • Only ESM exports are supported (Node.js/CommonJS exports are not supported).
    • Exported functions with arguments or generators are not supported.
    • Return values are dropped and will not be returned to the Wasm host.
    • The resulting Wasm module is a core Wasm module, not a Wasm component.

    To export functions, use the -C wit=<path> and -C wit-world=<name> flags during the build process.

    javy build index.js -C wit=index.wit -C wit-world=index-world -o index.wasm
  10. Use Javy to execute JavaScript in Rust

    main

    Javy is a configurable JavaScript runtime for WebAssembly that uses QuickJS via the rquickjs crate. It allows you to evaluate either JavaScript source code or QuickJS bytecode within a Rust environment.

    To use Javy, initialize a Runtime, create a context, and use the context.with method to interact with the JavaScript global scope. You can define Rust functions as JavaScript functions using Function::new and MutFn to expose them to the JS environment.

    use anyhow::Result;
    use javy::quickjs::{
       function::{MutFn, Rest},
       Ctx, Function, Value
    };
    use javy::{from_js_error, Runtime};
    
    fn main() -> Result<()> {
        let runtime = Runtime::default();
        let context = runtime.context();
    
        context.with(|cx| {
            let globals = cx.globals();
            globals.set(
                "print_hello",
                Function::new(
                    cx.clone(),
                    MutFn::new(|_: Ctx<'_>, _: Rest<Value<'_>>| {
                        println!("Hello, world!");
                    }),
                )?,
            )
        })?;
    
        context.with(|cx| {
            cx.eval_with_options("print_hello();", Default::default())
                .map_err(|e| from_js_error(cx.clone(), e))
                .map(|_: ()| ())
        })?;
    
        Ok(())
    }