javy_plugin_api/
lib.rs

1//! A crate for creating Javy plugins
2//!
3//! Example usage:
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//! The crate will automatically add exports for a number of Wasm functions in
37//! your crate that Javy needs to work.
38//!
39//! # Core concepts
40//! * [`javy`] - a re-export of the [`javy`] crate.
41//! * [`javy_plugin`] - Takes a namespace to use for the module name for
42//!   imports, a struct to add function exports to, a config method, and a
43//!   method for updating the Javy runtime.
44//! * [`Config`] - to add behavior to the created [`javy::Runtime`].
45//!
46//! # Features
47//! * `json` - enables the `json` feature in the `javy` crate.
48//! * `messagepack` - enables the `messagepack` feature in the `javy` crate.
49
50// Allow these in this file because we only run this program single threaded
51// and we can safely reason about the accesses to the Javy Runtime. We also
52// don't want to introduce overhead from taking unnecessary mutex locks.
53#![allow(static_mut_refs)]
54use anyhow::{anyhow, bail, Result};
55pub use config::Config;
56use javy::quickjs::{self, Ctx, Error as JSError, Function, Module, Value};
57use javy::{from_js_error, Runtime};
58use std::cell::OnceCell;
59use std::str;
60
61pub use javy;
62
63mod config;
64mod javy_plugin;
65mod namespace;
66
67const FUNCTION_MODULE_NAME: &str = "function.mjs";
68
69thread_local! {
70    static COMPILE_SRC_RET_AREA: OnceCell<[u32; 2]> = const { OnceCell::new() }
71}
72
73static mut RUNTIME: OnceCell<Runtime> = OnceCell::new();
74static mut EVENT_LOOP_ENABLED: bool = false;
75
76static EVENT_LOOP_ERR: &str = r#"
77                Pending jobs in the event queue.
78                Scheduling events is not supported when the 
79                event-loop runtime config is not enabled.
80            "#;
81
82/// Initializes the Javy runtime.
83pub fn initialize_runtime<F, G>(config: F, modify_runtime: G) -> Result<()>
84where
85    F: FnOnce() -> Config,
86    G: FnOnce(Runtime) -> Runtime,
87{
88    let config = config();
89    let runtime = Runtime::new(config.runtime_config)?;
90    let runtime = modify_runtime(runtime);
91    unsafe {
92        RUNTIME.take(); // Allow re-initializing.
93        RUNTIME
94            .set(runtime)
95            // `unwrap` requires error `T` to implement `Debug` but `set`
96            // returns the `javy::Runtime` on error and `javy::Runtime` does not
97            // implement `Debug`.
98            .map_err(|_| anyhow!("Could not pre-initialize javy::Runtime"))
99            .unwrap();
100        EVENT_LOOP_ENABLED = config.event_loop;
101    };
102    Ok(())
103}
104
105/// Compiles JS source code to QuickJS bytecode.
106///
107/// Returns result with the success value being a vector of the bytecode and
108/// failure being the error message.
109///
110/// # Arguments
111///
112/// * `config` - A function that returns a config for Javy
113/// * `modify_runtime` - A function that returns a Javy runtime
114/// * `js_src` - A slice of bytes representing the JS source code
115pub fn compile_src(js_src: &[u8]) -> Result<Vec<u8>> {
116    // Use initialized runtime when compiling because certain runtime
117    // configurations can cause different bytecode to be emitted.
118    //
119    // For example, given the following JS:
120    // ```
121    // function foo() {
122    //   "use math"
123    //   1234 % 32
124    // }
125    // ```
126    //
127    // Setting `config.bignum_extension` to `true` will produce different
128    // bytecode than if it were set to `false`.
129    let runtime = unsafe { RUNTIME.get().unwrap() };
130    runtime.compile_to_bytecode(FUNCTION_MODULE_NAME, &String::from_utf8_lossy(js_src))
131}
132
133/// Evaluates QuickJS bytecode and optionally invokes exported JS function with
134/// name.
135///
136/// # Arguments
137///
138/// * `bytecode` - The QuickJS bytecode
139/// * `fn_name` - The JS function name
140pub fn invoke(bytecode: &[u8], fn_name: Option<&str>) -> Result<()> {
141    let runtime = unsafe { RUNTIME.get() }.unwrap();
142    runtime
143        .context()
144        .with(|this| {
145            let module = unsafe { Module::load(this.clone(), bytecode)? };
146            let (module, promise) = module.eval()?;
147
148            handle_maybe_promise(this.clone(), promise.into())?;
149
150            if let Some(fn_name) = fn_name {
151                let fun: Function = module.get(fn_name)?;
152                // Exported functions are guaranteed not to have arguments so
153                // we can safely pass an empty tuple for arguments.
154                let value = fun.call(())?;
155                handle_maybe_promise(this.clone(), value)?
156            }
157            Ok(())
158        })
159        .map_err(|e| runtime.context().with(|cx| from_js_error(cx.clone(), e)))
160        .and_then(|_: ()| ensure_pending_jobs(runtime))
161}
162
163/// Handles the promise returned by evaluating the JS bytecode.
164fn handle_maybe_promise(this: Ctx, value: Value) -> quickjs::Result<()> {
165    match value.as_promise() {
166        Some(promise) => {
167            if unsafe { EVENT_LOOP_ENABLED } {
168                // If the event loop is enabled, trigger it.
169                let resolved = promise.finish::<Value>();
170                // `Promise::finish` returns Err(Wouldblock) when the all
171                // pending jobs have been handled.
172                if let Err(JSError::WouldBlock) = resolved {
173                    Ok(())
174                } else {
175                    resolved.map(|_| ())
176                }
177            } else {
178                // Else we simply expect the promise to resolve immediately.
179                match promise.result() {
180                    None => Err(javy::to_js_error(this, anyhow!(EVENT_LOOP_ERR))),
181                    Some(r) => r,
182                }
183            }
184        }
185        None => Ok(()),
186    }
187}
188
189fn ensure_pending_jobs(rt: &Runtime) -> Result<()> {
190    if unsafe { EVENT_LOOP_ENABLED } {
191        rt.resolve_pending_jobs()
192    } else if rt.has_pending_jobs() {
193        bail!(EVENT_LOOP_ERR);
194    } else {
195        Ok(())
196    }
197}