hyperlight_js_runtime/
lib.rs1#![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#[derive(Clone)]
31struct Handler<'a> {
32 func: Persistent<Function<'a>>,
33}
34
35pub struct JsRuntime {
38 context: Context,
39 handlers: HashMap<String, Handler<'static>>,
40}
41
42unsafe impl Send for JsRuntime {}
52
53impl JsRuntime {
54 #[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 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 host_loader.install(&ctx)?;
75
76 globals::setup(&ctx).catch(&ctx)
78 })?;
79
80 Ok(Self {
81 context,
82 handlers: HashMap::new(),
83 })
84 }
85
86 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 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 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 let handler_script = if !handler_script.contains("export") {
146 format!("{}\nexport {{ handler }};", handler_script)
147 } else {
148 handler_script
149 };
150
151 let handler_path = make_handler_path(&function_name, &handler_pwd);
153
154 let func = self.context.with(|ctx| -> anyhow::Result<_> {
155 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 let handler_func: Function = module.get("handler").catch(&ctx)?;
166
167 Ok(Persistent::save(&ctx, handler_func))
169 })?;
170
171 self.handlers.insert(function_name, Handler { func });
173
174 Ok(())
175 }
176
177 pub fn run_handler(
182 &mut self,
183 function_name: String,
184 event: String,
185 run_gc: bool,
186 ) -> anyhow::Result<String> {
187 let handler = self
189 .handlers
190 .get(&function_name)
191 .with_context(|| format!("No handler registered for function {function_name}"))?
192 .clone();
193
194 let _guard = FlushGuard;
197
198 self.context.with(|ctx| {
200 let _gc_guard = MaybeRunGcGuard::new(run_gc, &ctx);
202
203 let func = handler.func.clone().restore(&ctx).catch(&ctx)?;
205
206 let arg = ctx.json_parse(event).catch(&ctx)?;
208
209 let promise: MaybePromise = func.call((arg,)).catch(&ctx)?;
211 let obj: Value = promise.finish().catch(&ctx)?;
212
213 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 modules::io::io::flush();
227 self.handlers.clear();
230 }
231}
232
233#[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 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 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
302struct 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
327struct 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 self.ctx.run_gc();
348 }
349 }
350}