javy_plugin_api/
lib.rs

1//! A crate for creating Javy plugins
2//!
3//! Example usage for creating WASI preview2 plugins:
4//! ```ignore
5//! use javy_plugin_api::{
6//!     javy::{quickjs::prelude::Func, Runtime},
7//!     javy_plugin, Config,
8//! };
9//!
10//! wit_bindgen::generate!({ world: "my-javy-plugin-v1", generate_all });
11//!
12//! fn config() -> Config {
13//!     let mut config = Config::default();
14//!     config
15//!         .text_encoding(true)
16//!         .javy_stream_io(true);
17//!     config
18//! }
19//!
20//! fn modify_runtime(runtime: Runtime) -> Runtime {
21//!     runtime.context().with(|ctx| {
22//!         ctx.globals().set("plugin", true).unwrap();
23//!     });
24//!     runtime
25//! }
26//!
27//! struct Component;
28//!
29//! // Dynamically linked modules will use `my_javy_plugin_v1` as the import
30//! // namespace.
31//! javy_plugin!("my-javy-plugin-v1", Component, config, modify_runtime);
32//!
33//! export!(Component);
34//! ```
35//!
36//! //! Example for creating WASI preview 1 plugins:
37//! ```ignore
38//! use javy_plugin_api::{
39//!     import_namespace,
40//!     javy::{quickjs::prelude::Func, Runtime},
41//!     Config,
42//! };
43//!
44//! import_namespace!("test-plugin-wasip1");
45//!
46//! #[link(wasm_import_module = "some_host")]
47//! extern "C" {
48//!     fn imported_function();
49//! }
50//!
51//! fn config() -> Config {
52//!     Config::default()
53//! }
54//!
55//! fn modify_runtime(runtime: Runtime) -> Runtime {
56//!     runtime.context().with(|ctx| {
57//!         ctx.globals().set("plugin", true).unwrap();
58//!         ctx.globals()
59//!             .set(
60//!                 "func",
61//!                 Func::from(|| {
62//!                     unsafe { crate::imported_function() };
63//!                 }),
64//!             )
65//!             .unwrap();
66//!     });
67//!     runtime
68//! }
69//!
70//! #[export_name = "initialize-runtime"]
71//! fn initialize_runtime() {
72//!     javy_plugin_api::initialize_runtime(config, modify_runtime).unwrap()
73//! }
74//! ```
75//!
76//! The crate will automatically add exports for a number of Wasm functions in
77//! your crate that Javy needs to work.
78//!
79//! # Core concepts
80//! * [`javy`] - a re-export of the [`javy`] crate.
81//! * [`javy_plugin`] - Used for WASI preview 2 plugins and not WASI preview 1
82//!   plugins. Takes a namespace to use for the module name for imports, a
83//!   struct to add function exports to, a config method, and a method for
84//!   updating the Javy runtime.
85//! * [`import_namespace`] - Used for WASI preview 1 plugins. Takes a namespace
86//!   to use for the module name for imports.
87//! * [`Config`] - to add behavior to the created [`javy::Runtime`].
88//!
89//! # Features
90//! * `json` - enables the `json` feature in the `javy` crate.
91//! * `messagepack` - enables the `messagepack` feature in the `javy` crate.
92
93// Allow these in this file because we only run this program single threaded
94// and we can safely reason about the accesses to the Javy Runtime. We also
95// don't want to introduce overhead from taking unnecessary mutex locks.
96#![allow(static_mut_refs)]
97use anyhow::{Result, anyhow, bail};
98pub use config::Config;
99use javy::quickjs::{self, Ctx, Error as JSError, Function, Module, Value};
100use javy::{Runtime, from_js_error};
101use std::cell::OnceCell;
102use std::str;
103
104pub use javy;
105
106mod config;
107mod javy_plugin;
108mod namespace;
109#[cfg(all(target_family = "wasm", target_os = "wasi", target_env = "p1"))]
110mod wasi_p1;
111
112const FUNCTION_MODULE_NAME: &str = "function.mjs";
113
114thread_local! {
115    static COMPILE_SRC_RET_AREA: OnceCell<[u32; 2]> = const { OnceCell::new() }
116}
117
118static mut RUNTIME: OnceCell<Runtime> = OnceCell::new();
119static mut EVENT_LOOP_ENABLED: bool = false;
120
121static EVENT_LOOP_ERR: &str = r#"
122                Pending jobs in the event queue.
123                Scheduling events is not supported when the 
124                event-loop runtime config is not enabled.
125            "#;
126
127/// Initializes the Javy runtime.
128pub fn initialize_runtime<F, G>(config: F, modify_runtime: G) -> Result<()>
129where
130    F: FnOnce() -> Config,
131    G: FnOnce(Runtime) -> Runtime,
132{
133    let config = config();
134    let runtime = Runtime::new(config.runtime_config)?;
135    let runtime = modify_runtime(runtime);
136    unsafe {
137        RUNTIME.take(); // Allow re-initializing.
138        RUNTIME
139            .set(runtime)
140            // `unwrap` requires error `T` to implement `Debug` but `set`
141            // returns the `javy::Runtime` on error and `javy::Runtime` does not
142            // implement `Debug`.
143            .map_err(|_| anyhow!("Could not pre-initialize javy::Runtime"))
144            .unwrap();
145        EVENT_LOOP_ENABLED = config.event_loop;
146    };
147    Ok(())
148}
149
150/// Compiles JS source code to QuickJS bytecode.
151///
152/// Returns result with the success value being a vector of the bytecode and
153/// failure being the error message.
154///
155/// # Arguments
156///
157/// * `config` - A function that returns a config for Javy
158/// * `modify_runtime` - A function that returns a Javy runtime
159/// * `js_src` - A slice of bytes representing the JS source code
160pub fn compile_src(js_src: &[u8]) -> Result<Vec<u8>> {
161    // Use initialized runtime when compiling because certain runtime
162    // configurations can cause different bytecode to be emitted.
163    //
164    // For example, given the following JS:
165    // ```
166    // function foo() {
167    //   "use math"
168    //   1234 % 32
169    // }
170    // ```
171    //
172    // Setting `config.bignum_extension` to `true` will produce different
173    // bytecode than if it were set to `false`.
174    let runtime = unsafe { RUNTIME.get().unwrap() };
175    runtime.compile_to_bytecode(FUNCTION_MODULE_NAME, &String::from_utf8_lossy(js_src))
176}
177
178/// Evaluates QuickJS bytecode and optionally invokes exported JS function with
179/// name.
180///
181/// # Arguments
182///
183/// * `bytecode` - The QuickJS bytecode
184/// * `fn_name` - The JS function name
185pub fn invoke(bytecode: &[u8], fn_name: Option<&str>) -> Result<()> {
186    let runtime = unsafe { RUNTIME.get() }.unwrap();
187    runtime
188        .context()
189        .with(|this| {
190            let module = unsafe { Module::load(this.clone(), bytecode)? };
191            let (module, promise) = module.eval()?;
192
193            handle_maybe_promise(this.clone(), promise.into())?;
194
195            if let Some(fn_name) = fn_name {
196                let fun: Function = module.get(fn_name)?;
197                // Exported functions are guaranteed not to have arguments so
198                // we can safely pass an empty tuple for arguments.
199                let value = fun.call(())?;
200                handle_maybe_promise(this.clone(), value)?
201            }
202            Ok(())
203        })
204        .map_err(|e| runtime.context().with(|cx| from_js_error(cx.clone(), e)))
205        .and_then(|_: ()| ensure_pending_jobs(runtime))
206}
207
208/// Handles the promise returned by evaluating the JS bytecode.
209fn handle_maybe_promise(this: Ctx, value: Value) -> quickjs::Result<()> {
210    match value.as_promise() {
211        Some(promise) => {
212            if unsafe { EVENT_LOOP_ENABLED } {
213                // If the event loop is enabled, trigger it.
214                let resolved = promise.finish::<Value>();
215                // `Promise::finish` returns Err(Wouldblock) when the all
216                // pending jobs have been handled.
217                if let Err(JSError::WouldBlock) = resolved {
218                    Ok(())
219                } else {
220                    resolved.map(|_| ())
221                }
222            } else {
223                // Else we simply expect the promise to resolve immediately.
224                match promise.result() {
225                    None => Err(javy::to_js_error(this, anyhow!(EVENT_LOOP_ERR))),
226                    Some(r) => r,
227                }
228            }
229        }
230        None => Ok(()),
231    }
232}
233
234fn ensure_pending_jobs(rt: &Runtime) -> Result<()> {
235    if unsafe { EVENT_LOOP_ENABLED } {
236        rt.resolve_pending_jobs()
237    } else if rt.has_pending_jobs() {
238        bail!(EVENT_LOOP_ERR);
239    } else {
240        Ok(())
241    }
242}