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}