Skip to main content

hyperlight_js_runtime/
lib.rs

1#![no_std]
2#![no_main]
3extern crate alloc;
4
5mod globals;
6pub mod host;
7mod host_fn;
8mod modules;
9pub(crate) mod utils;
10
11use alloc::format;
12use alloc::rc::Rc;
13use alloc::string::{String, ToString};
14
15use anyhow::{anyhow, Context as _};
16use hashbrown::HashMap;
17use rquickjs::loader::{Loader, Resolver};
18use rquickjs::promise::MaybePromise;
19use rquickjs::{Context, Ctx, Function, Module, Persistent, Result, Runtime, Value};
20use serde::de::DeserializeOwned;
21use serde::Serialize;
22use tracing::instrument;
23
24use crate::host::Host;
25use crate::host_fn::{HostFunction, HostModuleLoader};
26use crate::modules::NativeModuleLoader;
27
28/// A handler is a javascript function that takes a single `event` object parameter,
29/// and is registered to the static `Context` instance
30#[derive(Clone)]
31struct Handler<'a> {
32    func: Persistent<Function<'a>>,
33}
34
35/// This is the main entry point for the library.
36/// It manages the QuickJS runtime, as well as the registered handlers and host modules.
37pub struct JsRuntime {
38    context: Context,
39    handlers: HashMap<String, Handler<'static>>,
40}
41
42// SAFETY:
43// This is safe. The reason it is not automatically implemented by the compiler
44// is because `rquickjs::Context` is not `Send` because it holds a raw pointer.
45// Raw pointers in rust are not marked as `Send` as lint rather than an actual
46// safety concern (see https://doc.rust-lang.org/nomicon/send-and-sync.html).
47// Moreover, rquickjs DOES implement Send for Context when the "parallel" feature
48// is enabled, further indicating that it is safe for this to implement `Send`.
49// Moreover, every public method of `JsRuntime` takes `&mut self`, and so we can
50// be certain that there are no concurrent accesses to it.
51unsafe impl Send for JsRuntime {}
52
53impl JsRuntime {
54    /// Create a new `JsRuntime` with the given host.
55    /// The resulting runtime will have global objects registered.
56    #[instrument(skip_all, level = "info")]
57    pub fn new<H: Host + 'static>(host: H) -> anyhow::Result<Self> {
58        let runtime = Runtime::new().context("Unable to initialize JS_RUNTIME")?;
59        let context = Context::full(&runtime).context("Unable to create JS context")?;
60
61        // Setup the module loader.
62        // We need to do this before setting up the globals as many of the globals are implemented
63        // as native modules, and so they need the module loader to be able to be loaded.
64        let host_loader = HostModuleLoader::default();
65        let native_loader = NativeModuleLoader;
66        let module_loader = ModuleLoader::new(host);
67
68        let loader = (host_loader.clone(), native_loader, module_loader);
69        runtime.set_loader(loader.clone(), loader);
70
71        context.with(|ctx| -> anyhow::Result<()> {
72            // we need to install the host loader in the context as the loader uses the context to
73            // store some global state needed for module instantiation.
74            host_loader.install(&ctx)?;
75
76            // Setup the global objects in the context, so they are available to the handler scripts.
77            globals::setup(&ctx).catch(&ctx)
78        })?;
79
80        Ok(Self {
81            context,
82            handlers: HashMap::new(),
83        })
84    }
85
86    /// Register a host function in the specified module.
87    /// The function takes and returns a JSON string, which is deserialized and serialized by the runtime.
88    /// The arguments are serialized as a JSON array containing all the arguments passed to the function.
89    pub fn register_json_host_function(
90        &mut self,
91        module_name: impl Into<String>,
92        function_name: impl Into<String>,
93        function: impl Fn(String) -> anyhow::Result<String> + 'static,
94    ) -> anyhow::Result<()> {
95        self.context.with(|ctx| {
96            ctx.userdata::<HostModuleLoader>()
97                .context("HostModuleLoader not found in context")?
98                .borrow_mut()
99                .entry(module_name.into())
100                .or_default()
101                .add_function(function_name.into(), HostFunction::new_json(function));
102            Ok(())
103        })
104    }
105
106    /// Register a host function in the specified module.
107    /// The function takes and returns any type that can be (de)serialized by `serde`.
108    pub fn register_host_function<Args, Output>(
109        &mut self,
110        module_name: impl Into<String>,
111        function_name: impl Into<String>,
112        function: impl fn_traits::Fn<Args, Output = anyhow::Result<Output>> + 'static,
113    ) -> anyhow::Result<()>
114    where
115        Args: DeserializeOwned,
116        Output: Serialize,
117    {
118        self.context.with(|ctx| {
119            ctx.userdata::<HostModuleLoader>()
120                .context("HostModuleLoader not found in context")?
121                .borrow_mut()
122                .entry(module_name.into())
123                .or_default()
124                .add_function(function_name.into(), HostFunction::new_serde(function));
125            Ok(())
126        })
127    }
128
129    /// Register a handler function with the runtime.
130    /// The handler script is a JavaScript module that exports a function named `handler`.
131    /// The handler function takes a single argument, which is the event data deserialized from a JSON string.
132    pub fn register_handler(
133        &mut self,
134        function_name: impl Into<String>,
135        handler_script: impl Into<String>,
136        handler_pwd: impl Into<String>,
137    ) -> anyhow::Result<()> {
138        let function_name = function_name.into();
139        let handler_script = handler_script.into();
140        let handler_pwd = handler_pwd.into();
141
142        // If the handler script doesn't already export the handler function, we export it for the user.
143        // This is a convenience for the common case where the handler script is just a single file that defines
144        // the handler function, without needing to explicitly export it.
145        let handler_script = if !handler_script.contains("export") {
146            format!("{}\nexport {{ handler }};", handler_script)
147        } else {
148            handler_script
149        };
150
151        // We create a "virtual" path for the handler module based on the function name and the provided handler directory.
152        let handler_path = make_handler_path(&function_name, &handler_pwd);
153
154        let func = self.context.with(|ctx| -> anyhow::Result<_> {
155            // Declare the module for the handler script, and evaluate it to get the exported handler function.
156            let module =
157                Module::declare(ctx.clone(), handler_path.as_str(), handler_script.clone())
158                    .catch(&ctx)?;
159
160            let (module, promise) = module.eval().catch(&ctx)?;
161
162            promise.finish::<()>().catch(&ctx)?;
163
164            // Get the exported handler function from the module namespace
165            let handler_func: Function = module.get("handler").catch(&ctx)?;
166
167            // Save the handler function as a Persistent so it can be returned outside of the `enter` closure.
168            Ok(Persistent::save(&ctx, handler_func))
169        })?;
170
171        // Store the handler function in the `handlers` map, so it can be called later when the handler is triggered.
172        self.handlers.insert(function_name, Handler { func });
173
174        Ok(())
175    }
176
177    /// Run a registered handler function with the given event data.
178    /// The event data is passed as a JSON string, and the handler function is expected to return a value that can be serialized to JSON.
179    /// The result is returned as a JSON string.
180    /// If `run_gc` is true, the runtime will run a garbage collection cycle after running the handler.
181    pub fn run_handler(
182        &mut self,
183        function_name: String,
184        event: String,
185        run_gc: bool,
186    ) -> anyhow::Result<String> {
187        // Get the handler function from the `handlers` map. If there is no handler registered for the given function name, return an error.
188        let handler = self
189            .handlers
190            .get(&function_name)
191            .with_context(|| format!("No handler registered for function {function_name}"))?
192            .clone();
193
194        // Create a guard that will flush any output when dropped (i.e., after running the handler).
195        // This makes sure that any output generated through libc is flushed out of the libc's stdout buffer.
196        let _guard = FlushGuard;
197
198        // Evaluate `handler(event)`, and get resulting object as String
199        self.context.with(|ctx| {
200            // Create a guard that will run a GC cycle when dropped if `run_gc` is true.
201            let _gc_guard = MaybeRunGcGuard::new(run_gc, &ctx);
202
203            // Restore the handler function from the Persistent reference.
204            let func = handler.func.clone().restore(&ctx).catch(&ctx)?;
205
206            // Call it with the event data parsed as a JSON value.
207            let arg = ctx.json_parse(event).catch(&ctx)?;
208
209            // If the handler returned a promise that resolves immediately, we resolve it.
210            let promise: MaybePromise = func.call((arg,)).catch(&ctx)?;
211            let obj: Value = promise.finish().catch(&ctx)?;
212
213            // Serialize the result to a JSON string and return it.
214            ctx.json_stringify(obj)
215                .catch(&ctx)?
216                .context("The handler function did not return a value")?
217                .to_string()
218                .catch(&ctx)
219        })
220    }
221}
222
223impl Drop for JsRuntime {
224    fn drop(&mut self) {
225        // make sure we flush any output when dropping the runtime
226        modules::io::io::flush();
227        // clear handlers to drop Persistent references before Context is dropped
228        // otherwise the runtime will abort on drop due to the memory leak.
229        self.handlers.clear();
230    }
231}
232
233// A module loader that calls out to the host to resolve and load modules
234#[derive(Clone)]
235struct ModuleLoader {
236    host: Rc<dyn Host>,
237}
238
239impl ModuleLoader {
240    fn new(host: impl Host + 'static) -> Self {
241        Self {
242            host: Rc::new(host),
243        }
244    }
245}
246
247impl Resolver for ModuleLoader {
248    fn resolve(&mut self, _ctx: &Ctx<'_>, base: &str, name: &str) -> Result<String> {
249        // quickjs uses the module path as the base for relative imports
250        // but oxc_resolver expects the directory as the base
251        let (dir, _) = base.rsplit_once('/').unwrap_or((".", ""));
252
253        let path = self
254            .host
255            .resolve_module(dir.to_string(), name.to_string())
256            .map_err(|_err| rquickjs::Error::new_resolving(base, name))?;
257
258        // convert backslashes to forward slashes for windows compatibility
259        let path = path.replace('\\', "/");
260        Ok(path)
261    }
262}
263
264impl Loader for ModuleLoader {
265    fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result<Module<'js>> {
266        let source = self
267            .host
268            .load_module(name.to_string())
269            .map_err(|_err| rquickjs::Error::new_loading(name))?;
270
271        Module::declare(ctx.clone(), name, source)
272    }
273}
274
275fn make_handler_path(function_name: &str, handler_dir: &str) -> String {
276    let handler_dir = if handler_dir.is_empty() {
277        "."
278    } else {
279        handler_dir
280    };
281
282    let function_name = if function_name.is_empty() {
283        "handler"
284    } else {
285        function_name
286    };
287
288    let function_name = function_name.replace('\\', "/");
289    let mut handler_path = handler_dir.replace('\\', "/");
290    if !handler_path.ends_with('/') {
291        handler_path.push('/');
292    }
293    handler_path.push_str(&function_name);
294
295    if !handler_path.ends_with(".js") && !handler_path.ends_with(".mjs") {
296        handler_path.push_str(".js");
297    }
298
299    handler_path
300}
301
302// RAII guard that flushes the output buffer of libc when dropped.
303// This is used to make sure we flush all output after running a handler, without needing to manually call it in every code path.
304struct FlushGuard;
305
306impl Drop for FlushGuard {
307    fn drop(&mut self) {
308        modules::io::io::flush();
309    }
310}
311
312trait CatchJsErrorExt {
313    type Ok;
314    fn catch(self, ctx: &Ctx<'_>) -> anyhow::Result<Self::Ok>;
315}
316
317impl<T> CatchJsErrorExt for rquickjs::Result<T> {
318    type Ok = T;
319    fn catch(self, ctx: &Ctx<'_>) -> anyhow::Result<T> {
320        match rquickjs::CatchResultExt::catch(self, ctx) {
321            Ok(s) => Ok(s),
322            Err(e) => Err(anyhow!("Runtime error: {e:#?}")),
323        }
324    }
325}
326
327// RAII guard that runs a GC cycle when dropped if `run_gc` is true.
328// This is used to make sure we run a GC cycle after running a handler if requested, without needing to manually call it in every code path.
329struct MaybeRunGcGuard<'a> {
330    run_gc: bool,
331    ctx: Ctx<'a>,
332}
333
334impl<'a> MaybeRunGcGuard<'a> {
335    fn new(run_gc: bool, ctx: &Ctx<'a>) -> Self {
336        Self {
337            run_gc,
338            ctx: ctx.clone(),
339        }
340    }
341}
342
343impl Drop for MaybeRunGcGuard<'_> {
344    fn drop(&mut self) {
345        if self.run_gc {
346            // safety: we are in the same context
347            self.ctx.run_gc();
348        }
349    }
350}