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}