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}