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