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
55static mut COMPILE_SRC_RET_AREA: [u32; 2] = [0; 2];
56
57static mut RUNTIME: OnceCell<Runtime> = OnceCell::new();
58static mut EVENT_LOOP_ENABLED: bool = false;
59
60static EVENT_LOOP_ERR: &str = r#"
61 Pending jobs in the event queue.
62 Scheduling events is not supported when the
63 event-loop runtime config is not enabled.
64 "#;
65
66/// Initializes the Javy runtime.
67pub fn initialize_runtime<F>(config: Config, modify_runtime: F) -> Result<()>
68where
69 F: FnOnce(Runtime) -> Runtime,
70{
71 let runtime = Runtime::new(config.runtime_config).unwrap();
72 let runtime = modify_runtime(runtime);
73 unsafe {
74 RUNTIME.take(); // Allow re-initializing.
75 RUNTIME
76 .set(runtime)
77 // `unwrap` requires error `T` to implement `Debug` but `set`
78 // returns the `javy::Runtime` on error and `javy::Runtime` does not
79 // implement `Debug`.
80 .map_err(|_| anyhow!("Could not pre-initialize javy::Runtime"))
81 .unwrap();
82 EVENT_LOOP_ENABLED = config.event_loop;
83 };
84 Ok(())
85}
86
87/// Compiles JS source code to QuickJS bytecode.
88///
89/// Returns a pointer to a buffer containing a 32-bit pointer to the bytecode byte array and the
90/// u32 length of the bytecode byte array.
91///
92/// # Arguments
93///
94/// * `js_src_ptr` - A pointer to the start of a byte array containing UTF-8 JS source code
95/// * `js_src_len` - The length of the byte array containing JS source code
96///
97/// # Safety
98///
99/// * `js_src_ptr` must reference a valid array of unsigned bytes of `js_src_len` length
100#[export_name = "compile_src"]
101pub unsafe extern "C" fn compile_src(js_src_ptr: *const u8, js_src_len: usize) -> *const u32 {
102 // Use initialized runtime when compiling because certain runtime
103 // configurations can cause different bytecode to be emitted.
104 //
105 // For example, given the following JS:
106 // ```
107 // function foo() {
108 // "use math"
109 // 1234 % 32
110 // }
111 // ```
112 //
113 // Setting `config.bignum_extension` to `true` will produce different
114 // bytecode than if it were set to `false`.
115 let runtime = unsafe { RUNTIME.get().unwrap() };
116 let js_src = str::from_utf8(slice::from_raw_parts(js_src_ptr, js_src_len)).unwrap();
117
118 let bytecode = runtime
119 .compile_to_bytecode(FUNCTION_MODULE_NAME, js_src)
120 .unwrap();
121
122 // We need the bytecode buffer to live longer than this function so it can be read from memory
123 let len = bytecode.len();
124 let bytecode_ptr = Box::leak(bytecode.into_boxed_slice()).as_ptr();
125 COMPILE_SRC_RET_AREA[0] = bytecode_ptr as u32;
126 COMPILE_SRC_RET_AREA[1] = len.try_into().unwrap();
127 COMPILE_SRC_RET_AREA.as_ptr()
128}
129
130/// Evaluates QuickJS bytecode and optionally invokes exported JS function with
131/// name.
132///
133/// # Safety
134///
135/// * `bytecode_ptr` must reference a valid array of bytes of `bytecode_len`
136/// length.
137/// * If `fn_name_ptr` is not 0, it must reference a UTF-8 string with
138/// `fn_name_len` byte length.
139#[export_name = "invoke"]
140pub unsafe extern "C" fn invoke(
141 bytecode_ptr: *const u8,
142 bytecode_len: usize,
143 fn_name_ptr: *const u8,
144 fn_name_len: usize,
145) {
146 let bytecode = slice::from_raw_parts(bytecode_ptr, bytecode_len);
147 let fn_name = if !fn_name_ptr.is_null() && fn_name_len != 0 {
148 Some(str::from_utf8_unchecked(slice::from_raw_parts(
149 fn_name_ptr,
150 fn_name_len,
151 )))
152 } else {
153 None
154 };
155 run_bytecode(bytecode, fn_name);
156}
157
158/// Evaluate the given bytecode.
159///
160/// Deprecated for use outside of this crate.
161///
162/// Evaluating also prepares (or "instantiates") the state of the JavaScript
163/// engine given all the information encoded in the bytecode.
164pub fn run_bytecode(bytecode: &[u8], fn_name: Option<&str>) {
165 let runtime = unsafe { RUNTIME.get() }.unwrap();
166 runtime
167 .context()
168 .with(|this| {
169 let module = unsafe { Module::load(this.clone(), bytecode)? };
170 let (module, promise) = module.eval()?;
171
172 handle_maybe_promise(this.clone(), promise.into())?;
173
174 if let Some(fn_name) = fn_name {
175 let fun: Function = module.get(fn_name)?;
176 // Exported functions are guaranteed not to have arguments so
177 // we can safely pass an empty tuple for arguments.
178 let value = fun.call(())?;
179 handle_maybe_promise(this.clone(), value)?
180 }
181 Ok(())
182 })
183 .map_err(|e| runtime.context().with(|cx| from_js_error(cx.clone(), e)))
184 .and_then(|_: ()| ensure_pending_jobs(runtime))
185 .unwrap_or_else(handle_error)
186}
187
188/// Handles the promise returned by evaluating the JS bytecode.
189fn handle_maybe_promise(this: Ctx, value: Value) -> quickjs::Result<()> {
190 match value.as_promise() {
191 Some(promise) => {
192 if unsafe { EVENT_LOOP_ENABLED } {
193 // If the event loop is enabled, trigger it.
194 let resolved = promise.finish::<Value>();
195 // `Promise::finish` returns Err(Wouldblock) when the all
196 // pending jobs have been handled.
197 if let Err(JSError::WouldBlock) = resolved {
198 Ok(())
199 } else {
200 resolved.map(|_| ())
201 }
202 } else {
203 // Else we simply expect the promise to resolve immediately.
204 match promise.result() {
205 None => Err(javy::to_js_error(this, anyhow!(EVENT_LOOP_ERR))),
206 Some(r) => r,
207 }
208 }
209 }
210 None => Ok(()),
211 }
212}
213
214fn ensure_pending_jobs(rt: &Runtime) -> Result<()> {
215 if unsafe { EVENT_LOOP_ENABLED } {
216 rt.resolve_pending_jobs()
217 } else if rt.has_pending_jobs() {
218 bail!(EVENT_LOOP_ERR);
219 } else {
220 Ok(())
221 }
222}
223
224fn handle_error(e: Error) {
225 eprintln!("{e}");
226 process::abort();
227}