Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Plugin support and dynamic linking changes for Javy #768

Closed
jeffcharles opened this issue Oct 1, 2024 · 1 comment
Closed

RFC: Plugin support and dynamic linking changes for Javy #768

jeffcharles opened this issue Oct 1, 2024 · 1 comment
Labels
enhancement New feature or request

Comments

@jeffcharles
Copy link
Collaborator

jeffcharles commented Oct 1, 2024

This is a proposal to add plugin support for Javy.

What problems does it solve?

At the moment, if someone wants to add custom JS APIs to Javy, they need to fork most of the project (javy-cli and javy-core). Ideally it would be possible to add custom JS APIs to Javy without forking anything.

We also have a problem with dynamically linked modules where import namespaces generated by the Javy CLI are rolled out before support for those import namespaces is available where they would be used. Specifically, to support using dynamically linked modules in a given runtime environment, the runtime environment needs to configure a linker with one or more Javy import namespaces (e.g., javy_quickjs_provider_v3) to load a QuickJS provider Wasm module. New releases of the Javy CLI can potentially change the import namespace used (in the future, a release could introduce a javy_quickjs_provider_v4 import namespace). However, a runtime environment using Javy would likely not be updated before the new release of the Javy CLI is available. So users using the latest Javy CLI release may create modules that are not compatible with that runtime environment.

As well, plugins should be able to specify their own import namespace because they may have their own JS APIs and the lifecycle of those APIs can be different than Javy CLI releases.

What are the changes?

  1. The javy build subcommand will accept a new -C plugin=plugin.wasm argument. The details of what's in the plugin.wasm file will be provided below.
  2. The -C plugin=plugin.wasm argument will be optional for the default statically linked modules. E.g., javy build -C plugin=my_plugin.wasm.
  3. The -C plugin=plugin.wasm argument will be required for the dynamically linked modules (this is a breaking change). E.g., javy build -C dynamic -C plugin=my_plugin.wasm.
  4. An initialize-plugin subcommand will be added to the Javy CLI. This command will need to be run to initialize a plugin before it can be used. This is intended to be run prior to distributing the plugin and not on an end user's machine. E.g., javy initialize-plugin path_to_myplugin.wasm -o=my_initialized_plugin.wasm.
  5. Change emit-provider to emit-plugin and apply -J options to the emitted plugin. E.g., javy emit-plugin.

Possible future changes

  1. Update emit-plugin to accept -J options if there is demand for allowing runtime configurations on the vanilla plugin.

Why these changes?

The new -C plugin option allows users to specify which plugin to use, if any. Creating a dynamically linked module will require specifying a plugin to make it much harder to accidentally generate a dynamically linked module which is incompatible with a given runtime environment. The requirement to provide a plugin will force the user to seek out a plugin as opposed to assuming the default will just work. The owner of the runtime environment can instruct their users as to where to find the plugin or provide their own tooling to download the appropriate plugin and configure Javy to use it.

Plugin module API

The plugin module is expected to be compiled as a wasm32-wasi cdylib. The module must have the following exports (details provided below):

  • initialize_runtime() -> ()
  • canonical_abi_realloc(i32, i32, i32, i32) -> i32
  • compile_src(i32, i32) -> i32
  • invoke(i32, i32, i32, i32) -> ()

And the following custom section:

  • import_namespace containing a UTF-8 encoded string

The initialize_runtime function

This function will be responsible for adding additional APIs to the JS runtime exposed by Javy. This function will also be responsible for forwarding or modifying user-provided JS runtime configurations.

An simplified example of what an implementation could look like is:

#[export_name = "initialize_runtime"]
pub extern "C" fn initialize_runtime() {
    let user_config = javy::exported_fns::user_config();
    javy::exported_fns::initialize_runtime(user_config, |runtime| {
        unsafe { JavyJson::add_intrinsic(runtime.context().as_raw()) };
        runtime
    });
}

where JavyJson is a copy of the Javy.JSON implementation.

The javy::exported_fns::user_config() returns a javy::Config representing the -J options passed on the command line and can be modified. javy::exported_fns::initialize_runtime takes the configuration and a callback for configuring an initialized runtime.

This isn't the finalized API, the exports from the Javy crate will likely look slightly different.

The import_namespace custom section

  • Contains a UTF-8 encoded string with the import namespace to use for the module when generating a dynamically linked module.

An example of how to configure this would be to have code that looks like:

const fn byte_string_len(s: &[u8]) -> usize {
    s.len()
}

#[link_section = "import_namespace"]
pub static IMPORT_NAMESPACE: [u8; byte_string_len(b"quickjs_provider_v3")] =
    *b"quickjs_provider_v3";

This will likely be simplified into a macro exported from the javy crate.

Exports from Javy javy-plugins-api crate

The following functions can be exported into the plugin module from the javy javy-api-plugins crate and are expected to be present:

  • canonical_abi_realloc(original_ptr: i32, original_size: i32, alignment: i32, new_size: i32) -> ret_ptr: i32
    • Allocates memory of new_size with alignment in the plugin
  • compile_src(js_src_ptr: i32, js_src_len: i32) -> ret_ptr: i32
    • Compiles JS source code in the byte array specified by js_src_ptr and js_src_len to QuickJS bytecode using the JS runtime initialized by initialize_runtime
  • invoke(bytecode_ptr: i32, bytecode_len: i32, fn_name_ptr: i32, fn_name_len: i32) -> ()
    • Executes the bytecode in the byte array specified by bytecode_ptr and bytecode_len, if fn_name_ptr and fn_name_len are not 0, then executes the JS function with the name specified in the UTF-8 encoded string specified by fn_name_ptr and fn_name_len

No code is expected to be written by plugin developers to satisfy these APIs, they will be automatically exported from the plugin module by using the javy javy-plugins-api crate with a feature flag enabled.


Taken together, these changes should allow the creation of plugins providing custom JS APIs and the ability to version or otherwise change the import namespace of the plugin independently of the code in this Javy repository.

@jeffcharles
Copy link
Collaborator Author

This has been fully implemented!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant