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}