diff --git a/Cargo.toml b/Cargo.toml index 55b0388..e54b34d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = ["src/hyperlight-js", "src/js-host-api", "src/hyperlight-js-runtime"] +exclude = ["src/hyperlight-js-runtime/tests/fixtures/extended_runtime"] [workspace.package] version = "0.1.1" diff --git a/Justfile b/Justfile index 95c9ac9..0b95183 100644 --- a/Justfile +++ b/Justfile @@ -135,6 +135,8 @@ clean: cargo clean cd src/hyperlight-js-runtime && cargo clean cd src/js-host-api && cargo clean + -rm -rf src/hyperlight-js-runtime/tests/fixtures/extended_runtime/target + -rm -rf src/hyperlight-js-runtime/tests/fixtures/native_math/target -rm -rf src/js-host-api/node_modules -rm -f src/js-host-api/*.node -rm -f src/js-host-api/index.js @@ -150,15 +152,52 @@ test target=default-target features="": (build target) cd src/hyperlight-js && cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} handle_termination --profile={{ if target == "debug" {"dev"} else { target } }} -- --ignored --nocapture cd src/hyperlight-js && cargo test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} test_metrics --profile={{ if target == "debug" {"dev"} else { target } }} -- --ignored --nocapture cargo test --manifest-path=./src/hyperlight-js-runtime/Cargo.toml --test=native_cli --profile={{ if target == "debug" {"dev"} else { target } }} + just test-native-modules {{ target }} # Test with monitor features enabled (wall-clock and CPU time monitors) -# Note: We exclude test_metrics as it requires process isolation and is already run by `test` recipe +# Note: We exclude test_metrics (requires process isolation, already run by `test`) +# and native_modules (requires custom guest runtime, run by `test-native-modules`) test-monitors target=default-target: - cd src/hyperlight-js && cargo test --features monitor-wall-clock,monitor-cpu-time --profile={{ if target == "debug" {"dev"} else { target } }} -- --include-ignored --skip test_metrics + cd src/hyperlight-js && cargo test --features monitor-wall-clock,monitor-cpu-time --profile={{ if target == "debug" {"dev"} else { target } }} -- --include-ignored --skip test_metrics --skip custom_native_module --skip builtin_modules_work_with_custom --skip console_log_works_with_custom test-js-host-api target=default-target features="": (build-js-host-api target features) cd src/js-host-api && npm test +# Test custom native modules: +# 1. Runs the runtime crate's native_modules unit/pipeline tests (native binary) +# 2. Builds the extended_runtime fixture for the hyperlight target +# 3. Rebuilds hyperlight-js with the custom guest embedded via HYPERLIGHT_JS_RUNTIME_PATH +# 4. Runs the ignored VM integration tests +# 5. Rebuilds hyperlight-js with the default guest (unsets HYPERLIGHT_JS_RUNTIME_PATH) +# +# The build.rs in hyperlight-js has `cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_PATH` +# so setting/unsetting the env var triggers a rebuild automatically. + +# Base path to the extended runtime fixture target directory +extended_runtime_target := replace(justfile_dir(), "\\", "/") + "/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/target/x86_64-hyperlight-none" + +test-native-modules target=default-target: (ensure-tools) (_test-native-modules-unit target) (_test-native-modules-build-guest target) (_test-native-modules-vm target) (_test-native-modules-restore target) + +[private] +_test-native-modules-unit target=default-target: + cargo test --manifest-path=./src/hyperlight-js-runtime/Cargo.toml --test=native_modules --profile={{ if target == "debug" {"dev"} else { target } }} + +[private] +_test-native-modules-build-guest target=default-target: + cargo hyperlight build \ + --manifest-path src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml \ + --profile={{ if target == "debug" {"dev"} else { target } }} \ + --target-dir src/hyperlight-js-runtime/tests/fixtures/extended_runtime/target + +[private] +_test-native-modules-vm target=default-target: + {{ set-env-command }}HYPERLIGHT_JS_RUNTIME_PATH="{{extended_runtime_target}}/{{ if target == "debug" {"debug"} else { target } }}/extended-runtime" {{ if os() == "windows" { ";" } else { "&&" } }} cargo test -p hyperlight-js --test native_modules --profile={{ if target == "debug" {"dev"} else { target } }} -- --ignored --nocapture + +[private] +_test-native-modules-restore target=default-target: + @echo "Rebuilding hyperlight-js with default guest runtime..." + cd src/hyperlight-js && cargo build --profile={{ if target == "debug" {"dev"} else { target } }} + # Run js-host-api examples (simple.js, calculator.js, unload.js, interrupt.js, cpu-timeout.js, host-functions.js) run-js-host-api-examples target=default-target features="": (build-js-host-api target features) @echo "Running js-host-api examples..." diff --git a/docs/extending-runtime.md b/docs/extending-runtime.md new file mode 100644 index 0000000..9c58582 --- /dev/null +++ b/docs/extending-runtime.md @@ -0,0 +1,209 @@ +# Extending the Runtime with Custom Native Modules + +This document describes how to extend `hyperlight-js-runtime` with custom +native (Rust-implemented) modules that run alongside the built-in modules +inside the Hyperlight guest VM. + +## Why Native Modules? 🤔 + +Some operations are too slow in pure JavaScript. For example, DEFLATE +compression can be 50–100× slower than native Rust, which may trigger CPU +timeouts on large inputs. Native modules let you add high-performance Rust +code that JavaScript handlers can `import` — without forking the runtime. + +## How It Works + +1. **`hyperlight-js-runtime` as a library** — the runtime crate exposes a + `[lib]` target so your crate can depend on it. +2. **`native_modules!` macro** — registers custom modules into a global + registry. The runtime's `NativeModuleLoader` checks custom modules + first, then falls back to built-ins (io, crypto, console, require). +3. **`HYPERLIGHT_JS_RUNTIME_PATH`** — a build-time env var that tells + `hyperlight-js` to embed your custom runtime binary instead of the + default one. + +## Quick Start + +### 1. Create your custom runtime crate + +```bash +cargo init --bin my-custom-runtime +``` + +```toml +[dependencies] +hyperlight-js-runtime = { git = "https://github.com/hyperlight-dev/hyperlight-js" } +rquickjs = { version = "0.11", default-features = false, features = ["bindgen", "futures", "macro", "loader"] } + +# Only needed for native CLI testing, not the hyperlight guest +[target.'cfg(not(hyperlight))'.dependencies] +anyhow = "1.0" + +[lints.rust] +unexpected_cfgs = { level = "allow", check-cfg = ['cfg(hyperlight)'] } +``` + +> **Note:** The `rquickjs` version and features must match what +> `hyperlight-js-runtime` uses. Check its `Cargo.toml` for the exact spec. + +### 2. Define your module and register it + +```rust +#![cfg_attr(hyperlight, no_std)] +#![cfg_attr(hyperlight, no_main)] + +#[rquickjs::module(rename_vars = "camelCase")] +mod math { + #[rquickjs::function] + pub fn add(a: f64, b: f64) -> f64 { a + b } + + #[rquickjs::function] + pub fn multiply(a: f64, b: f64) -> f64 { a * b } +} + +hyperlight_js_runtime::native_modules! { + "math" => js_math, +} +``` + +That's all the Rust you write for the Hyperlight guest. The macro generates +an `init_native_modules()` function that the `NativeModuleLoader` calls +automatically on first use. Built-in modules are inherited. The lib provides +all hyperlight guest infrastructure (entry point, host function dispatch, +libc stubs) — no copying files or build scripts needed. + +### 3. Build and embed in hyperlight-js + +Build your custom runtime for the Hyperlight target and embed it: + +```bash +# Build for the hyperlight target +cargo hyperlight build --manifest-path my-custom-runtime/Cargo.toml + +# Build hyperlight-js with your custom runtime embedded +HYPERLIGHT_JS_RUNTIME_PATH=/path/to/my-custom-runtime \ + cargo build -p hyperlight-js +``` + +### 4. Use from the host + +The host-side code is **identical** to any other `hyperlight-js` usage. +Custom native modules are transparent — they're baked into the guest +binary. Your handlers just `import` from them: + +```rust +use hyperlight_js::{SandboxBuilder, Script}; + +fn main() -> anyhow::Result<()> { + let proto = SandboxBuilder::new().build()?; + let mut sandbox = proto.load_runtime()?; + + let handler = Script::from_content(r#" + import { add, multiply } from "math"; + export function handler(event) { + return { + sum: add(event.a, event.b), + product: multiply(event.a, event.b), + }; + } + "#); + sandbox.add_handler("compute", handler)?; + + let mut loaded = sandbox.get_loaded_sandbox()?; + let result = loaded.handle_event("compute", r#"{"a":6,"b":7}"#.to_string(), None)?; + + println!("{result}"); + // {"sum":13,"product":42} + + Ok(()) +} +``` + +### 5. Test natively (optional) + +For local development you can run your custom runtime as a native CLI +without building for Hyperlight. Add a `main()` to your `main.rs`. + +Since your custom modules are registered via the macro (and built-ins are +handled by the runtime), you don't need filesystem module resolution (But you can have it if you want it). +A no-op `Host` is all that's needed — it only gets called for `.js` file +imports, which native modules don't use: + +```rust +struct NoOpHost; +impl hyperlight_js_runtime::host::Host for NoOpHost { + fn resolve_module(&self, _base: String, name: String) -> anyhow::Result { + anyhow::bail!("Module '{name}' not found") + } + fn load_module(&self, name: String) -> anyhow::Result { + anyhow::bail!("Module '{name}' not found") + } +} + +fn main() -> anyhow::Result<()> { + let args: Vec = std::env::args().collect(); + let script = std::fs::read_to_string(&args[1])?; + + let mut runtime = hyperlight_js_runtime::JsRuntime::new(NoOpHost)?; + runtime.register_handler("handler", script, ".")?; + let result = runtime.run_handler("handler".into(), args[2].clone(), false)?; + println!("{result}"); + Ok(()) +} +``` + +```bash +# handler.js +cat > handler.js << 'EOF' +import { add, multiply } from "math"; +export function handler(event) { + return { sum: add(event.a, event.b), product: multiply(event.a, event.b) }; +} +EOF + +cargo run -- handler.js '{"a":6,"b":7}' +# {"sum":13,"product":42} +``` + +## Complete Example + +See the [extended_runtime fixture](../src/hyperlight-js-runtime/tests/fixtures/extended_runtime/) +for a working example with end-to-end tests. + +Run `just test-native-modules` to build the fixture for the Hyperlight +target and run the full integration tests. + +## API Reference + +### `native_modules!` + +```rust +hyperlight_js_runtime::native_modules! { + "module_name" => ModuleDefType, + "another" => AnotherModuleDefType, +} +``` + +Generates an `init_native_modules()` function that registers the listed +modules into the global native module registry. Called automatically by the +`NativeModuleLoader` on first use — you never need to call it yourself. +Built-in modules are inherited automatically. + +**Restrictions:** +- Custom module names **cannot** shadow built-in modules (`io`, `crypto`, + `console`, `require`). Attempting to register a built-in name panics. + +### `register_native_module` + +```rust +hyperlight_js_runtime::modules::register_native_module(name, declaration_fn) +``` + +Register a single custom native module by name. Typically called via the +`native_modules!` macro rather than directly. + +### `JsRuntime::new` + +```rust +hyperlight_js_runtime::JsRuntime::new(host) +``` diff --git a/src/hyperlight-js-runtime/Cargo.toml b/src/hyperlight-js-runtime/Cargo.toml index 0b22909..799b55a 100644 --- a/src/hyperlight-js-runtime/Cargo.toml +++ b/src/hyperlight-js-runtime/Cargo.toml @@ -7,11 +7,16 @@ license.workspace = true repository.workspace = true readme.workspace = true description = """ -hyperlight-js-runtime is a rust binary crate that provides the JavaScript runtime binary for hyperlight-js. +hyperlight-js-runtime provides the JavaScript runtime for hyperlight-js, both as a library and binary. """ +[lib] +name = "hyperlight_js_runtime" +path = "src/lib.rs" + [[bin]] name = "hyperlight-js-runtime" +path = "src/main.rs" harness = false test = false diff --git a/src/hyperlight-js-runtime/src/main/hyperlight.rs b/src/hyperlight-js-runtime/src/guest/mod.rs similarity index 83% rename from src/hyperlight-js-runtime/src/main/hyperlight.rs rename to src/hyperlight-js-runtime/src/guest/mod.rs index 350ab50..617d63f 100644 --- a/src/hyperlight-js-runtime/src/main/hyperlight.rs +++ b/src/hyperlight-js-runtime/src/guest/mod.rs @@ -13,11 +13,23 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -extern crate alloc; + +//! Hyperlight guest entry point and infrastructure. +//! +//! This module provides the guest-side plumbing needed to run the JS runtime +//! inside a Hyperlight VM. It includes: +//! - The `Host` implementation that calls out to hyperlight host functions +//! - The `hyperlight_main` entry point +//! - Guest function registrations (register_handler, RegisterHostModules) +//! - The `guest_dispatch_function` fallback for handler calls +//! - Libc stub implementations required by QuickJS +//! +//! This is all `cfg(hyperlight)` — compiled out entirely for native builds. use alloc::format; use alloc::string::String; use alloc::vec::Vec; + use anyhow::{anyhow, Context as _}; use hashbrown::HashMap; use hyperlight_common::flatbuffer_wrappers::function_call::FunctionCall; @@ -33,7 +45,7 @@ mod stubs; struct Host; -pub trait CatchGuestErrorExt { +trait CatchGuestErrorExt { type Ok; fn catch(self) -> anyhow::Result; } @@ -45,7 +57,7 @@ impl CatchGuestErrorExt for hyperlight_guest::error::Result { } } -impl hyperlight_js_runtime::host::Host for Host { +impl crate::host::Host for Host { fn resolve_module(&self, base: String, name: String) -> anyhow::Result { #[host_function("ResolveModule")] fn resolve_module(base: String, name: String) -> Result; @@ -65,8 +77,8 @@ impl hyperlight_js_runtime::host::Host for Host { } } -static RUNTIME: spin::Lazy> = spin::Lazy::new(|| { - Mutex::new(hyperlight_js_runtime::JsRuntime::new(Host).unwrap_or_else(|e| { +static RUNTIME: spin::Lazy> = spin::Lazy::new(|| { + Mutex::new(crate::JsRuntime::new(Host).unwrap_or_else(|e| { panic!("Failed to initialize JS runtime: {e:#?}"); })) }); @@ -74,8 +86,7 @@ static RUNTIME: spin::Lazy> = spin::Lazy #[unsafe(no_mangle)] #[instrument(skip_all, level = "info")] pub extern "C" fn hyperlight_main() { - // dereference RUNTIME to force its initialization - // of the Lazy static + // Initialise the runtime (custom modules are registered lazily on first use) let _ = &*RUNTIME; } diff --git a/src/hyperlight-js-runtime/src/main/stubs/clock.rs b/src/hyperlight-js-runtime/src/guest/stubs/clock.rs similarity index 100% rename from src/hyperlight-js-runtime/src/main/stubs/clock.rs rename to src/hyperlight-js-runtime/src/guest/stubs/clock.rs diff --git a/src/hyperlight-js-runtime/src/main/stubs/io.rs b/src/hyperlight-js-runtime/src/guest/stubs/io.rs similarity index 100% rename from src/hyperlight-js-runtime/src/main/stubs/io.rs rename to src/hyperlight-js-runtime/src/guest/stubs/io.rs diff --git a/src/hyperlight-js-runtime/src/main/stubs/localtime.rs b/src/hyperlight-js-runtime/src/guest/stubs/localtime.rs similarity index 100% rename from src/hyperlight-js-runtime/src/main/stubs/localtime.rs rename to src/hyperlight-js-runtime/src/guest/stubs/localtime.rs diff --git a/src/hyperlight-js-runtime/src/main/stubs/mod.rs b/src/hyperlight-js-runtime/src/guest/stubs/mod.rs similarity index 100% rename from src/hyperlight-js-runtime/src/main/stubs/mod.rs rename to src/hyperlight-js-runtime/src/guest/stubs/mod.rs diff --git a/src/hyperlight-js-runtime/src/main/stubs/srand.rs b/src/hyperlight-js-runtime/src/guest/stubs/srand.rs similarity index 100% rename from src/hyperlight-js-runtime/src/main/stubs/srand.rs rename to src/hyperlight-js-runtime/src/guest/stubs/srand.rs diff --git a/src/hyperlight-js-runtime/src/lib.rs b/src/hyperlight-js-runtime/src/lib.rs index 4d89e2e..e49dda2 100644 --- a/src/hyperlight-js-runtime/src/lib.rs +++ b/src/hyperlight-js-runtime/src/lib.rs @@ -14,14 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ #![no_std] -#![no_main] extern crate alloc; mod globals; +/// Hyperlight guest entry point — provides `hyperlight_main`, +/// `guest_dispatch_function`, and all guest plumbing. +/// Only compiled when building for the Hyperlight VM target. +#[cfg(hyperlight)] +mod guest; pub mod host; mod host_fn; mod libc; -mod modules; +/// Native module infrastructure for the JS runtime. +/// +/// Contains the built-in native modules (io, crypto, console, require) +/// and the [`native_modules!`] macro for extending the runtime with +/// custom native modules in downstream crates. +pub mod modules; pub(crate) mod utils; use alloc::format; @@ -30,6 +39,7 @@ use alloc::string::{String, ToString}; use anyhow::{anyhow, Context as _}; use hashbrown::HashMap; +use modules::NativeModuleLoader; use rquickjs::loader::{Loader, Resolver}; use rquickjs::promise::MaybePromise; use rquickjs::{Context, Ctx, Function, Module, Persistent, Result, Runtime, Value}; @@ -39,7 +49,6 @@ use tracing::instrument; use crate::host::Host; use crate::host_fn::{HostFunction, HostModuleLoader}; -use crate::modules::NativeModuleLoader; /// A handler is a javascript function that takes a single `event` object parameter, /// and is registered to the static `Context` instance @@ -68,7 +77,11 @@ unsafe impl Send for JsRuntime {} impl JsRuntime { /// Create a new `JsRuntime` with the given host. - /// The resulting runtime will have global objects registered. + /// + /// The runtime includes all built-in native modules (io, crypto, console, + /// require) plus any custom modules registered via + /// [`register_native_module`](modules::register_native_module) or the + /// [`native_modules!`] macro. #[instrument(skip_all, level = "info")] pub fn new(host: H) -> anyhow::Result { let runtime = Runtime::new().context("Unable to initialize JS_RUNTIME")?; diff --git a/src/hyperlight-js-runtime/src/main.rs b/src/hyperlight-js-runtime/src/main.rs index b981090..b2abae9 100644 --- a/src/hyperlight-js-runtime/src/main.rs +++ b/src/hyperlight-js-runtime/src/main.rs @@ -16,11 +16,15 @@ limitations under the License. #![cfg_attr(hyperlight, no_std)] #![cfg_attr(hyperlight, no_main)] -#[cfg(hyperlight)] -mod libc; +// Provide the `init_native_modules` symbol required by the NativeModuleLoader. +// The upstream binary has no custom modules so this is empty. +// Extender binaries list their custom modules here instead. +// See: docs/extending-runtime.md +hyperlight_js_runtime::native_modules! {} -#[cfg(hyperlight)] -include!("main/hyperlight.rs"); +// The hyperlight guest entry point (hyperlight_main, guest_dispatch_function, +// etc.) is provided by the lib's `guest` module. +// The binary only needs to provide the native CLI entry point. #[cfg(not(hyperlight))] include!("main/native.rs"); diff --git a/src/hyperlight-js-runtime/src/modules/mod.rs b/src/hyperlight-js-runtime/src/modules/mod.rs index c0ca64f..e98dd8d 100644 --- a/src/hyperlight-js-runtime/src/modules/mod.rs +++ b/src/hyperlight-js-runtime/src/modules/mod.rs @@ -19,35 +19,29 @@ use hashbrown::HashMap; use rquickjs::loader::{Loader, Resolver}; use rquickjs::module::ModuleDef; use rquickjs::{Ctx, Module, Result}; -use spin::Lazy; +use spin::{Lazy, Mutex}; -pub mod console; -pub mod crypto; -pub mod io; -pub mod require; +pub(crate) mod console; +pub(crate) mod crypto; +pub(crate) mod io; +pub(crate) mod require; -// A loader for native Rust modules -#[derive(Clone)] -pub struct NativeModuleLoader; +/// A function pointer type for declaring a native module. +#[doc(hidden)] +pub type ModuleDeclarationFn = for<'js> fn(Ctx<'js>, &str) -> Result>; -/// A function pointer type for declaring a module. -type ModuleDeclarationFn = for<'js> fn(Ctx<'js>, &str) -> Result>; - -/// This function returns a function pointer that when called declares a module -/// of type M. -/// Doing `declaration::()(ctx, "some_name")` is technically the same as -/// doing `Module::declare_def::(ctx, "some_name")`. -/// However, if we try to get a function pointer from `Module::declare_def::` directly, -/// we get issues due to lifetime conflicts. This function works around that conflict -/// by explicitly defining the lifetimes and returning a function pointer with the correct signature. -fn declaration() -> ModuleDeclarationFn { +/// Returns a function pointer that declares a module of type `M`. +#[doc(hidden)] +pub fn declaration() -> ModuleDeclarationFn { fn declare<'js, M: ModuleDef>(ctx: Ctx<'js>, name: &str) -> Result> { Module::declare_def::(ctx, name) } declare:: } -static NATIVE_MODULES: Lazy> = Lazy::new(|| { +// ── Built-in modules ─────────────────────────────────────────────────────── + +static BUILTIN_MODULES: Lazy> = Lazy::new(|| { HashMap::from([ ("io", declaration::()), ("crypto", declaration::()), @@ -56,9 +50,69 @@ static NATIVE_MODULES: Lazy> = Lazy::new(|| { ]) }); +/// Returns the names of all built-in native modules. +pub fn builtin_module_names() -> alloc::vec::Vec<&'static str> { + BUILTIN_MODULES.keys().copied().collect() +} + +// ── Custom module registry ───────────────────────────────────────────────── +// +// Extender crates register their custom native modules here via +// `register_native_module`. The NativeModuleLoader checks this registry +// first, then falls back to the built-in modules. + +static CUSTOM_MODULES: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + +/// Register a custom native module by name. +/// +/// The module will be available to JavaScript via `import { ... } from "name"`. +/// Custom modules cannot shadow built-in modules (io, crypto, console, require). +/// +/// This is typically called via the [`native_modules!`] macro rather than +/// directly. +/// +/// # Panics +/// +/// Panics if `name` collides with a built-in module name. +pub fn register_native_module(name: &'static str, decl: ModuleDeclarationFn) { + if BUILTIN_MODULES.contains_key(name) { + panic!( + "Cannot register custom native module '{name}': name conflicts with a built-in module" + ); + } + CUSTOM_MODULES.lock().insert(name, decl); +} + +// Flag to ensure custom modules are initialised before the loader is used. +// The init_native_modules symbol is provided by the binary crate via the +// native_modules! macro. We call it lazily on first loader access so that +// neither the native CLI nor extender binaries need to call it explicitly. +static CUSTOM_MODULES_INIT: spin::Once = spin::Once::new(); + +fn ensure_custom_modules_init() { + CUSTOM_MODULES_INIT.call_once(|| { + unsafe extern "Rust" { + fn init_native_modules(); + } + unsafe { init_native_modules() }; + }); +} + +// ── NativeModuleLoader ───────────────────────────────────────────────────── + +/// The unified loader for all native (Rust-implemented) modules. +/// +/// Checks the custom module registry first (populated via +/// [`register_native_module`] or [`native_modules!`]), then falls back to +/// the built-in modules (io, crypto, console, require). +#[derive(Clone)] +pub struct NativeModuleLoader; + impl Resolver for NativeModuleLoader { fn resolve(&mut self, _ctx: &Ctx<'_>, base: &str, name: &str) -> Result { - if NATIVE_MODULES.contains_key(name) { + ensure_custom_modules_init(); + if CUSTOM_MODULES.lock().contains_key(name) || BUILTIN_MODULES.contains_key(name) { Ok(name.to_string()) } else { Err(rquickjs::Error::new_resolving(base, name)) @@ -68,10 +122,51 @@ impl Resolver for NativeModuleLoader { impl Loader for NativeModuleLoader { fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result> { - if let Some(declaration) = NATIVE_MODULES.get(name) { - declaration(ctx.clone(), name) - } else { - Err(rquickjs::Error::new_loading(name)) + ensure_custom_modules_init(); + // Check custom modules first + if let Some(decl) = CUSTOM_MODULES.lock().get(name) { + return decl(ctx.clone(), name); + } + // Fall back to built-in modules + if let Some(decl) = BUILTIN_MODULES.get(name) { + return decl(ctx.clone(), name); } + Err(rquickjs::Error::new_loading(name)) } } + +/// Register custom native modules and generate the `init_native_modules` +/// entry point that the hyperlight guest calls during startup. +/// +/// # Example +/// +/// ```rust,ignore +/// #[rquickjs::module(rename_vars = "camelCase")] +/// mod math { +/// #[rquickjs::function] +/// pub fn add(a: f64, b: f64) -> f64 { a + b } +/// } +/// +/// hyperlight_js_runtime::native_modules! { +/// "math" => js_math, +/// } +/// ``` +/// +/// Custom module names **cannot** shadow built-in modules (`io`, `crypto`, +/// `console`, `require`). Attempting to do so will panic at startup. +#[macro_export] +macro_rules! native_modules { + ($($name:expr => $module:ty),* $(,)?) => { + /// Called by the hyperlight guest entry point to register custom + /// native modules before the JS runtime is initialised. + #[unsafe(no_mangle)] + pub fn init_native_modules() { + $( + $crate::modules::register_native_module( + $name, + $crate::modules::declaration::<$module>(), + ); + )* + } + }; +} diff --git a/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.lock b/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.lock new file mode 100644 index 0000000..7f78ff0 --- /dev/null +++ b/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.lock @@ -0,0 +1,1022 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "buddy_system_allocator" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672b945a3e4f4f40bfd4cd5ee07df9e796a42254ce7cd6d2599ad969244c44a" +dependencies = [ + "spin", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "extended-runtime" +version = "0.0.0" +dependencies = [ + "anyhow", + "hyperlight-js-runtime", + "native-math", + "rquickjs", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" +dependencies = [ + "bitflags", + "rustc_version", +] + +[[package]] +name = "fn-traits" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63fa6a0487cbc51bbe29ca4fd89c0bccc18eca48e7cb750b61d0828074ba40d0" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hyperlight-common" +version = "0.12.0" +source = "git+https://github.com/hyperlight-dev/hyperlight?rev=620339aa95d508e8cbd1d38b4374f09090aade7b#620339aa95d508e8cbd1d38b4374f09090aade7b" +dependencies = [ + "anyhow", + "flatbuffers", + "log", + "spin", + "thiserror", +] + +[[package]] +name = "hyperlight-guest" +version = "0.12.0" +source = "git+https://github.com/hyperlight-dev/hyperlight?rev=620339aa95d508e8cbd1d38b4374f09090aade7b#620339aa95d508e8cbd1d38b4374f09090aade7b" +dependencies = [ + "anyhow", + "flatbuffers", + "hyperlight-common", + "serde_json", + "tracing", +] + +[[package]] +name = "hyperlight-guest-bin" +version = "0.12.0" +source = "git+https://github.com/hyperlight-dev/hyperlight?rev=620339aa95d508e8cbd1d38b4374f09090aade7b#620339aa95d508e8cbd1d38b4374f09090aade7b" +dependencies = [ + "buddy_system_allocator", + "cc", + "cfg-if", + "flatbuffers", + "glob", + "hyperlight-common", + "hyperlight-guest", + "hyperlight-guest-macro", + "hyperlight-guest-tracing", + "linkme", + "log", + "spin", + "tracing", +] + +[[package]] +name = "hyperlight-guest-macro" +version = "0.12.0" +source = "git+https://github.com/hyperlight-dev/hyperlight?rev=620339aa95d508e8cbd1d38b4374f09090aade7b#620339aa95d508e8cbd1d38b4374f09090aade7b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hyperlight-guest-tracing" +version = "0.12.0" +source = "git+https://github.com/hyperlight-dev/hyperlight?rev=620339aa95d508e8cbd1d38b4374f09090aade7b#620339aa95d508e8cbd1d38b4374f09090aade7b" +dependencies = [ + "hyperlight-common", + "spin", + "tracing", + "tracing-core", +] + +[[package]] +name = "hyperlight-js-runtime" +version = "0.1.1" +dependencies = [ + "anyhow", + "base64", + "bindgen", + "chrono", + "clap", + "fn-traits", + "hashbrown", + "hex", + "hmac", + "hyperlight-common", + "hyperlight-guest", + "hyperlight-guest-bin", + "rquickjs", + "serde", + "serde_json", + "sha2", + "spin", + "tracing", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linkme" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e3283ed2d0e50c06dd8602e0ab319bb048b6325d0bba739db64ed8205179898" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5cec0ec4228b4853bb129c84dbf093a27e6c7a20526da046defc334a1b017f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "native-math" +version = "0.0.0" +dependencies = [ + "rquickjs", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" +dependencies = [ + "serde", +] + +[[package]] +name = "rquickjs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50dc6d6c587c339edb4769cf705867497a2baf0eca8b4645fa6ecd22f02c77a" +dependencies = [ + "rquickjs-core", + "rquickjs-macro", +] + +[[package]] +name = "rquickjs-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bf7840285c321c3ab20e752a9afb95548c75cd7f4632a0627cea3507e310c1" +dependencies = [ + "async-lock", + "hashbrown", + "relative-path", + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-macro" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7106215ff41a5677b104906a13e1a440b880f4b6362b5dc4f3978c267fad2b80" +dependencies = [ + "convert_case", + "fnv", + "ident_case", + "indexmap", + "proc-macro-crate", + "proc-macro2", + "quote", + "rquickjs-core", + "syn", +] + +[[package]] +name = "rquickjs-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27344601ef27460e82d6a4e1ecb9e7e99f518122095f3c51296da8e9be2b9d83" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml b/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml new file mode 100644 index 0000000..a003e90 --- /dev/null +++ b/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "extended-runtime" +version = "0.0.0" +edition = "2024" +publish = false + +# Prevent cargo from treating this as part of the parent workspace +[workspace] + +[[bin]] +name = "extended-runtime" +path = "src/main.rs" + +[dependencies] +hyperlight-js-runtime = { path = "../../.." } +native-math = { path = "../native_math" } +rquickjs = { version = "0.11", default-features = false, features = ["bindgen", "futures", "macro", "loader"] } + +# anyhow is only needed for the native CLI entry point, not the hyperlight guest +[target.'cfg(not(hyperlight))'.dependencies] +anyhow = "1.0" + +[lints.rust] +unexpected_cfgs = { level = "allow", check-cfg = ['cfg(hyperlight)'] } diff --git a/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/src/main.rs b/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/src/main.rs new file mode 100644 index 0000000..27ca931 --- /dev/null +++ b/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/src/main.rs @@ -0,0 +1,69 @@ +/* +Copyright 2026 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! An extended runtime binary that demonstrates adding custom native modules +//! to hyperlight-js-runtime using the `native_modules!` macro. +//! +//! This binary works for both native (CLI) testing and as a Hyperlight guest. +//! The lib provides all hyperlight guest infrastructure — no copying needed. + +#![cfg_attr(hyperlight, no_std)] +#![cfg_attr(hyperlight, no_main)] + +// Use the shared math module +use native_math::js_math; + +// Register "math" into the global native module registry. +// Built-in modules (io, crypto, console, require) are inherited automatically. +hyperlight_js_runtime::native_modules! { + "math" => js_math, +} + +// ── Native CLI entry point (for dev/testing) ─────────────────────────────── + +#[cfg(not(hyperlight))] +fn main() -> anyhow::Result<()> { + use std::path::Path; + use std::{env, fs}; + + let args: Vec = env::args().collect(); + let file = std::path::PathBuf::from(&args[1]); + let event = &args[2]; + + let handler_script = fs::read_to_string(&file)?; + let handler_pwd = file.parent().unwrap_or_else(|| Path::new(".")); + env::set_current_dir(handler_pwd)?; + + struct NoOpHost; + impl hyperlight_js_runtime::host::Host for NoOpHost { + fn resolve_module(&self, _base: String, name: String) -> anyhow::Result { + anyhow::bail!("Module '{name}' not found") + } + fn load_module(&self, name: String) -> anyhow::Result { + anyhow::bail!("Module '{name}' not found") + } + } + + let mut runtime = hyperlight_js_runtime::JsRuntime::new(NoOpHost)?; + runtime.register_handler("handler", handler_script, ".")?; + + let result = runtime.run_handler("handler".into(), event.clone(), false)?; + println!("Handler result: {result}"); + Ok(()) +} + +// For hyperlight builds: the lib's `guest` module provides hyperlight_main, +// guest_dispatch_function, and all plumbing. Nothing else needed here. diff --git a/src/hyperlight-js-runtime/tests/fixtures/native_math/Cargo.toml b/src/hyperlight-js-runtime/tests/fixtures/native_math/Cargo.toml new file mode 100644 index 0000000..4fc2df1 --- /dev/null +++ b/src/hyperlight-js-runtime/tests/fixtures/native_math/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "native-math" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +name = "native_math" +path = "src/lib.rs" + +# Prevent cargo from treating this as part of the parent workspace +[workspace] + +[dependencies] +rquickjs = { version = "0.11", default-features = false, features = ["bindgen", "futures", "macro", "loader"] } + +[lints.rust] +unexpected_cfgs = { level = "allow", check-cfg = ['cfg(hyperlight)'] } diff --git a/src/hyperlight-js-runtime/tests/fixtures/native_math/src/lib.rs b/src/hyperlight-js-runtime/tests/fixtures/native_math/src/lib.rs new file mode 100644 index 0000000..137c031 --- /dev/null +++ b/src/hyperlight-js-runtime/tests/fixtures/native_math/src/lib.rs @@ -0,0 +1,34 @@ +/* +Copyright 2026 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#![cfg_attr(hyperlight, no_std)] + +//! A custom native module providing basic math operations. +//! Used as a test fixture for the native module extension system. + +#[rquickjs::module(rename_vars = "camelCase")] +pub mod math { + /// Add two numbers. + #[rquickjs::function] + pub fn add(a: f64, b: f64) -> f64 { + a + b + } + + /// Multiply two numbers. + #[rquickjs::function] + pub fn multiply(a: f64, b: f64) -> f64 { + a * b + } +} diff --git a/src/hyperlight-js-runtime/tests/native_modules.rs b/src/hyperlight-js-runtime/tests/native_modules.rs new file mode 100644 index 0000000..127f405 --- /dev/null +++ b/src/hyperlight-js-runtime/tests/native_modules.rs @@ -0,0 +1,447 @@ +/* +Copyright 2026 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! Tests for the native module extension system. +//! +//! Verifies that: +//! - `register_native_module` adds custom modules to the loader +//! - Built-in modules always work +//! - Custom modules can be imported from JS handlers +//! - Built-in module names cannot be overridden (panics) +//! - The `native_modules!` macro generates correct `init_native_modules` +//! - Full pipeline tests with the extended_runtime fixture binary + +#![cfg(not(hyperlight))] + +use rquickjs::loader::{Loader, Resolver}; + +/// Helper: create a QuickJS runtime + context and run a closure within it. +fn with_qjs_context(f: impl FnOnce(rquickjs::Ctx<'_>)) { + let rt = rquickjs::Runtime::new().unwrap(); + let ctx = rquickjs::Context::full(&rt).unwrap(); + ctx.with(f); +} + +/// A minimal Host that doesn't support loading external modules. +struct NoOpHost; + +impl hyperlight_js_runtime::host::Host for NoOpHost { + fn resolve_module(&self, _base: String, name: String) -> anyhow::Result { + anyhow::bail!("NoOpHost does not support resolving module '{name}'") + } + + fn load_module(&self, name: String) -> anyhow::Result { + anyhow::bail!("NoOpHost does not support loading module '{name}'") + } +} + +// ── NativeModuleLoader: built-in modules ─────────────────────────────────── + +#[test] +fn loader_resolves_all_builtin_modules() { + let mut loader = hyperlight_js_runtime::modules::NativeModuleLoader; + let builtins = hyperlight_js_runtime::modules::builtin_module_names(); + assert!( + !builtins.is_empty(), + "Should have at least one built-in module" + ); + + with_qjs_context(|ctx| { + for name in &builtins { + let result = loader.resolve(&ctx, ".", name); + assert!(result.is_ok(), "Should resolve built-in module '{name}'"); + assert_eq!(result.unwrap(), *name); + } + }); +} + +#[test] +fn loader_rejects_unknown_modules() { + let mut loader = hyperlight_js_runtime::modules::NativeModuleLoader; + + with_qjs_context(|ctx| { + let result = loader.resolve(&ctx, ".", "nonexistent"); + assert!(result.is_err(), "Should reject unknown modules"); + }); +} + +#[test] +fn loader_loads_all_builtin_modules() { + let mut loader = hyperlight_js_runtime::modules::NativeModuleLoader; + let builtins = hyperlight_js_runtime::modules::builtin_module_names(); + + with_qjs_context(|ctx| { + for name in &builtins { + let result = loader.load(&ctx, name); + assert!( + result.is_ok(), + "Should load built-in module '{name}', got: {:?}", + result.err() + ); + } + }); +} + +// ── register_native_module ───────────────────────────────────────────────── + +/// A trivial test module that exports a single `greet` function. +#[rquickjs::module(rename_vars = "camelCase")] +mod test_greeting { + #[rquickjs::function] + pub fn greet() -> String { + String::from("hello from test module") + } +} + +#[test] +fn registered_custom_module_resolves_and_loads() { + hyperlight_js_runtime::modules::register_native_module( + "greeting", + hyperlight_js_runtime::modules::declaration::(), + ); + + let mut loader = hyperlight_js_runtime::modules::NativeModuleLoader; + + with_qjs_context(|ctx| { + let result = loader.resolve(&ctx, ".", "greeting"); + assert!( + result.is_ok(), + "Should resolve registered 'greeting' module" + ); + + let result = loader.load(&ctx, "greeting"); + assert!( + result.is_ok(), + "Should load registered 'greeting' module, got: {:?}", + result.err() + ); + }); +} + +#[test] +fn builtins_still_work_after_custom_registration() { + // Register a custom module first, then verify builtins still work + hyperlight_js_runtime::modules::register_native_module( + "greeting", + hyperlight_js_runtime::modules::declaration::(), + ); + + let mut loader = hyperlight_js_runtime::modules::NativeModuleLoader; + let builtins = hyperlight_js_runtime::modules::builtin_module_names(); + + with_qjs_context(|ctx| { + for name in &builtins { + let result = loader.resolve(&ctx, ".", name); + assert!( + result.is_ok(), + "Built-in '{name}' should still resolve after custom registration" + ); + } + }); +} + +// ── Override prevention ──────────────────────────────────────────────────── + +#[test] +#[should_panic(expected = "conflicts with a built-in module")] +fn registering_builtin_name_panics() { + #[rquickjs::module(rename_vars = "camelCase")] + mod fake_io { + #[rquickjs::function] + pub fn print(_txt: String) {} + } + + hyperlight_js_runtime::modules::register_native_module( + "io", + hyperlight_js_runtime::modules::declaration::(), + ); +} + +// ── native_modules! macro ────────────────────────────────────────────────── + +#[rquickjs::module(rename_vars = "camelCase")] +mod test_math { + #[rquickjs::function] + pub fn add(a: f64, b: f64) -> f64 { + a + b + } +} + +// The macro generates init_native_modules() which calls register_native_module +hyperlight_js_runtime::native_modules! { + "test_math_macro" => js_test_math, +} + +#[test] +fn macro_generated_init_registers_modules() { + init_native_modules(); + + let mut loader = hyperlight_js_runtime::modules::NativeModuleLoader; + + with_qjs_context(|ctx| { + let result = loader.resolve(&ctx, ".", "test_math_macro"); + assert!( + result.is_ok(), + "Module registered via native_modules! macro should resolve" + ); + }); +} + +// ── End-to-end JsRuntime tests ───────────────────────────────────────────── + +#[test] +fn e2e_handler_imports_custom_native_module() { + // Register our test module (idempotent — HashMap insert is safe to repeat) + hyperlight_js_runtime::modules::register_native_module( + "greeting", + hyperlight_js_runtime::modules::declaration::(), + ); + + let mut runtime = + hyperlight_js_runtime::JsRuntime::new(NoOpHost).expect("Failed to create JsRuntime"); + + let handler_script = r#" + import { greet } from "greeting"; + export function handler(event) { + return greet(); + } + "#; + + runtime + .register_handler("test_handler", handler_script, ".") + .expect("Failed to register handler"); + + let result = runtime + .run_handler("test_handler".to_string(), "{}".to_string(), false) + .expect("Failed to run handler"); + + assert_eq!(result, "\"hello from test module\""); +} + +#[test] +fn e2e_handler_uses_builtin_and_custom_modules_together() { + hyperlight_js_runtime::modules::register_native_module( + "greeting", + hyperlight_js_runtime::modules::declaration::(), + ); + + let mut runtime = + hyperlight_js_runtime::JsRuntime::new(NoOpHost).expect("Failed to create JsRuntime"); + + let handler_script = r#" + import { greet } from "greeting"; + import { createHmac } from "crypto"; + export function handler(event) { + const greeting = greet(); + const hmac = createHmac("sha256", "key"); + hmac.update("data"); + const digest = hmac.digest("hex"); + return { greeting, hasDigest: digest.length > 0 }; + } + "#; + + runtime + .register_handler("combo_handler", handler_script, ".") + .expect("Failed to register handler"); + + let result = runtime + .run_handler("combo_handler".to_string(), "{}".to_string(), false) + .expect("Failed to run handler"); + + let parsed: serde_json::Value = serde_json::from_str(&result).expect("Invalid JSON result"); + assert_eq!(parsed["greeting"], "hello from test module"); + assert_eq!(parsed["hasDigest"], true); +} + +#[test] +fn e2e_default_runtime_builtins_work() { + let mut runtime = + hyperlight_js_runtime::JsRuntime::new(NoOpHost).expect("Failed to create JsRuntime"); + + let handler_script = r#" + import { createHmac } from "crypto"; + export function handler(event) { + const hmac = createHmac("sha256", "secret"); + hmac.update(event.data); + return hmac.digest("hex"); + } + "#; + + runtime + .register_handler("default_handler", handler_script, ".") + .expect("Failed to register handler"); + + let result = runtime + .run_handler( + "default_handler".to_string(), + r#"{"data":"test"}"#.to_string(), + false, + ) + .expect("Failed to run handler"); + + let hex_str = result.trim_matches('"'); + assert_eq!( + hex_str.len(), + 64, + "Expected 64-char hex digest, got: {result}" + ); + assert!( + hex_str.chars().all(|c| c.is_ascii_hexdigit()), + "Expected hex string" + ); +} + +// ── Full pipeline E2E tests ──────────────────────────────────────────────── + +use std::path::PathBuf; +use std::process::Command; +use std::sync::LazyLock; + +const EXTENDED_RUNTIME_MANIFEST: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/extended_runtime/Cargo.toml" +); + +static EXTENDED_RUNTIME_BINARY: LazyLock = LazyLock::new(|| { + let output = Command::new("cargo") + .args(["build", "--manifest-path", EXTENDED_RUNTIME_MANIFEST]) + .output() + .expect("Failed to run cargo build for extended-runtime fixture"); + + assert!( + output.status.success(), + "Failed to build extended-runtime fixture:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + + let fixture_dir = PathBuf::from(EXTENDED_RUNTIME_MANIFEST) + .parent() + .unwrap() + .to_path_buf(); + let binary_name = if cfg!(windows) { + "extended-runtime.exe" + } else { + "extended-runtime" + }; + let binary = fixture_dir.join("target/debug").join(binary_name); + assert!(binary.exists(), "Binary not found at {binary:?}"); + binary +}); + +#[test] +fn full_pipeline_custom_native_module() { + let binary = &*EXTENDED_RUNTIME_BINARY; + let dir = tempfile::tempdir().unwrap(); + + std::fs::write( + dir.path().join("handler.js"), + r#" + import { add, multiply } from "math"; + export function handler(event) { + return { sum: add(event.a, event.b), product: multiply(event.a, event.b) }; + } + "#, + ) + .unwrap(); + + let output = Command::new(binary) + .arg(dir.path().join("handler.js")) + .arg(r#"{"a":6,"b":7}"#) + .output() + .unwrap(); + + assert!( + output.status.success(), + "Failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains(r#"{"sum":13,"product":42}"#), + "Got: {stdout}" + ); +} + +#[test] +fn full_pipeline_custom_and_builtin_modules_together() { + let binary = &*EXTENDED_RUNTIME_BINARY; + let dir = tempfile::tempdir().unwrap(); + + std::fs::write( + dir.path().join("handler.js"), + r#" + import { add } from "math"; + import { createHmac } from "crypto"; + export function handler(event) { + const sum = add(event.a, event.b); + const hmac = createHmac("sha256", "key"); + hmac.update("data"); + const digest = hmac.digest("hex"); + return { sum, digestLength: digest.length }; + } + "#, + ) + .unwrap(); + + let output = Command::new(binary) + .arg(dir.path().join("handler.js")) + .arg(r#"{"a":10,"b":32}"#) + .output() + .unwrap(); + + assert!( + output.status.success(), + "Failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains(r#""sum":42"#), "Got: {stdout}"); + assert!(stdout.contains(r#""digestLength":64"#), "Got: {stdout}"); +} + +#[test] +fn full_pipeline_console_log_with_custom_modules() { + let binary = &*EXTENDED_RUNTIME_BINARY; + let dir = tempfile::tempdir().unwrap(); + + std::fs::write( + dir.path().join("handler.js"), + r#" + import { multiply } from "math"; + function handler(event) { + const result = multiply(event.x, event.y); + console.log("computed: " + result); + return result; + } + "#, + ) + .unwrap(); + + let output = Command::new(binary) + .arg(dir.path().join("handler.js")) + .arg(r#"{"x":6,"y":9}"#) + .output() + .unwrap(); + + assert!( + output.status.success(), + "Failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + let lines: Vec<&str> = stdout.trim().lines().collect(); + assert_eq!(lines, ["computed: 54", "Handler result: 54"]); +} diff --git a/src/hyperlight-js/build.rs b/src/hyperlight-js/build.rs index d188ea1..412d02d 100644 --- a/src/hyperlight-js/build.rs +++ b/src/hyperlight-js/build.rs @@ -193,7 +193,17 @@ fn build_js_runtime() -> PathBuf { } fn bundle_runtime() { - let js_runtime_resource = build_js_runtime(); + let js_runtime_resource = match env::var("HYPERLIGHT_JS_RUNTIME_PATH") { + Ok(path) => { + println!("cargo:warning=Using custom JS runtime: {}", path); + println!("cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_PATH"); + println!("cargo:rerun-if-changed={}", path); + PathBuf::from(path) + .canonicalize() + .expect("HYPERLIGHT_JS_RUNTIME_PATH must point to a valid file") + } + Err(_) => build_js_runtime(), + }; let out_dir = env::var_os("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("host_resource.rs"); diff --git a/src/hyperlight-js/tests/native_modules.rs b/src/hyperlight-js/tests/native_modules.rs new file mode 100644 index 0000000..28dc0f6 --- /dev/null +++ b/src/hyperlight-js/tests/native_modules.rs @@ -0,0 +1,137 @@ +/* +Copyright 2026 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! Integration tests for custom native modules in the Hyperlight VM. +//! +//! These tests require a custom runtime (the `extended_runtime` fixture) +//! built for `x86_64-hyperlight-none` and embedded in `hyperlight-js` via +//! `HYPERLIGHT_JS_RUNTIME_PATH`. They are marked `#[ignore]` because they +//! cannot run with a normal `cargo test`. +//! +//! To run them, use: +//! ```bash +//! just test-native-modules +//! ``` +//! +//! This recipe builds the fixture with `cargo hyperlight build`, sets the +//! env var, rebuilds `hyperlight-js` with the custom guest, and runs these +//! tests. + +#![allow(clippy::disallowed_macros)] + +use hyperlight_js::{SandboxBuilder, Script}; + +/// Test that a custom native module ("math") can be imported and used +/// from a handler running inside the Hyperlight VM. +#[test] +#[ignore] +fn custom_native_module_works_in_vm() { + let handler = Script::from_content( + r#" + import { add, multiply } from "math"; + export function handler(event) { + return { + sum: add(event.a, event.b), + product: multiply(event.a, event.b), + }; + } + "#, + ); + + let mut sandbox = SandboxBuilder::new() + .build() + .unwrap() + .load_runtime() + .unwrap(); + + sandbox.add_handler("compute", handler).unwrap(); + + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + let result = loaded + .handle_event("compute", r#"{"a":6,"b":7}"#.to_string(), None) + .unwrap(); + + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["sum"], 13.0); + assert_eq!(parsed["product"], 42.0); +} + +/// Test that built-in modules still work alongside custom native modules. +#[test] +#[ignore] +fn builtin_modules_work_with_custom_native_module() { + let handler = Script::from_content( + r#" + import { add } from "math"; + import { createHmac } from "crypto"; + export function handler(event) { + const sum = add(event.a, event.b); + const hmac = createHmac("sha256", "key"); + hmac.update("data"); + const digest = hmac.digest("hex"); + return { sum, digestLength: digest.length }; + } + "#, + ); + + let mut sandbox = SandboxBuilder::new() + .build() + .unwrap() + .load_runtime() + .unwrap(); + + sandbox.add_handler("combo", handler).unwrap(); + + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + let result = loaded + .handle_event("combo", r#"{"a":10,"b":32}"#.to_string(), None) + .unwrap(); + + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["sum"], 42.0); + assert_eq!(parsed["digestLength"], 64); +} + +/// Test that console.log works alongside custom native modules. +#[test] +#[ignore] +fn console_log_works_with_custom_native_module() { + let handler = Script::from_content( + r#" + import { multiply } from "math"; + export function handler(event) { + const result = multiply(event.x, event.y); + console.log("computed: " + result); + return result; + } + "#, + ); + + let mut sandbox = SandboxBuilder::new() + .build() + .unwrap() + .load_runtime() + .unwrap(); + + sandbox.add_handler("log_test", handler).unwrap(); + + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + let result = loaded + .handle_event("log_test", r#"{"x":6,"y":9}"#.to_string(), None) + .unwrap(); + + assert_eq!(result, "54"); +}