Skip to content

Latest commit

 

History

History
199 lines (145 loc) · 6.03 KB

File metadata and controls

199 lines (145 loc) · 6.03 KB

Extending

If you want to use Javy for your own project, you may find that the existing code is not sufficient since you may want to offer custom JavaScript APIs. The approach we recommend taking is to create your own Javy plugin Wasm module. This plugin module can then be specified when using the Javy CLI when building Javy Wasm modules as a -C plugin flag when using javy build.

To create your own Javy plugin Wasm module, create a new Rust project that will compile to a library (that is, cargo init --lib). Javy plugins are written as Wasm components but converted to Wasm modules during the initialization process.

Your Cargo.toml should look like the following data :

[package]
name = "my-plugin-name"
version = "0.1.0"

[lib]
name = "my_plugin_name"
crate-type = ["cdylib"]

[dependencies]
javy-plugin-api = "4.0.0"
wit-bindgen = "0.44.0"

You'll need a WIT file in wit/world.wit that looks like the following code :

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>);
}

If you want to use hostcalls in your plugin, you'll also need to include imports in your world. For example, if you wanted to import a function named imported-function that takes no arguments and doesn't return anything, it'll look like :

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>);

    import imported-function: func();
}

Since Javy's plugin initialization process converts Wasm components to Wasm modules, parameter and result types will be represented as their lowered core Wasm equivalents. You can also use s32, f32, s64, and f64 as the parameter and result types and use the exported cabi_realloc Wasm function to handle structured data (arrays, strings) as you would have with core Wasm modules. See our documentation on using complex data types in Wasm functions for more details.

For the world with the imported function, the src/lib.rs should look like :

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();
        // Creates an `importedFunc` function on the global which will call
        // the imported function.
        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);

If you do not want to use the javy_plugin! macro for whatever reason, you can use the underlying APIs in src/lib.rs directly :

use std::process;

use javy_plugin_api::javy::Runtime;
use javy_plugin_api::{import_namespace, Config};

wit_bindgen::generate!({ world: "my-javy-plugin", generate_all });

// Set your plugin's import namespace.
import_namespace!("my-javy-plugin");

struct Component;

impl Guest for Component {
    fn invoke(bytecode: Vec<u8>, function: Option<String>) {
        javy_plugin_api::invoke(&bytecode, function.as_deref()).unwrap_or_else(|e| {
            eprintln!("{e}");
            process::abort();
        })
    }

    fn compile_src(src: Vec<u8>) -> Result<Vec<u8>, String> {
        javy_plugin_api::compile_src(&src).map_err(|e| e.to_string())
    }

    fn initialize_runtime() {
        javy_plugin_api::initialize_runtime(config, modify_runtime).unwrap()
    }
}

export!(Component);

fn config() -> Config {
    Config::default()
    
}

fn modify_runtime(runtime: Runtime) -> Runtime {
    runtime
}

You can then run cargo build --target=wasm32-wasip2 --release to create a Wasm module. Then you need to run

javy init-plugin <path_to_plugin> -o <path_to_initialized_module>`

which will validate and initialize the Javy runtime. This javy init-plugin step is required for the plugin to be useable by the Javy CLI.

Migration to v2.0.0 of javy-plugin-api

Consult the javy-plugin-api README.

The full plugin API

This is the Wasm API the Javy CLI expects Javy plugins to expose. The javy-plugin! macro defines default implementations of these functions.

Exported Wasm functions

initialize_runtime() -> ()

This will initialize a mutable global containing the Javy runtime for use by compile_src and invoke.

cabi_realloc(orig_ptr: i32, orig_len: i32, new_ptr: i32, new_len: i32) -> ptr: i32

This is used to allocate memory in the plugin module.

compile_src(src_ptr: i32, src_len: i32) -> result_wide_ptr: i32

This is used to compile JavaScript source code to QuickJS bytecode. The return pointer points to a result type of (discriminator: i32, ptr: i32, len: i32) in the plugin instance's linear memory. If discriminator is 0, ptr and len are the offset and length of the QuickJS bytecode. If the discriminator is 1, ptr and len are the offset and length of a UTF-8 string containing an error message.

invoke(bytecode_ptr: i32, bytecode_len: i32, fn_name_discriminator: i32, fn_name_ptr: i32, fn_name_len: i32) -> ()

This is used to evaluate the JavaScript code and optionally to call an exported JS function if fn_name_discriminator is not 0.

Custom sections

import_namespace

Contains a UTF-8 encoded string. This is used to determine the namespace that will be used for the Wasm imports in dynamically linked modules built with this plugin.