Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 7 additions & 26 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ latest-release:= if os() == "windows" {"$(git tag -l --sort=v:refname | select -
PWD := replace(justfile_dir(), "\\", "/")

# Set the HYPERLIGHT_CFLAGS so cargo-hyperlight applies them when building the runtimes:
# * include the stubs required by hyperlight-js-runtime
# * define __wasi__ as this disables threading support in quickjs
export HYPERLIGHT_CFLAGS := \
"-I" + PWD + "/src/hyperlight-js-runtime/include " + \
"-D__wasi__=1 " + \
"-D_POSIX_MONOTONIC_CLOCK "
export HYPERLIGHT_CFLAGS := "-D__wasi__=1 -D_POSIX_MONOTONIC_CLOCK"

# On Windows, use Ninja generator for CMake to avoid aws-lc-sys build issues with Visual Studio generator
export CMAKE_GENERATOR := if os() == "windows" { "Ninja" } else { "" }
Expand Down Expand Up @@ -160,40 +156,25 @@ test target=default-target features="": (build target)
# 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 --skip custom_native_module --skip builtin_modules_work_with_custom --skip console_log_works_with_custom
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 --skip custom_globals_and_host_clock

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.
# 2. Builds and embeds the fixture from its manifest and runs the VM tests
# 3. Rebuilds hyperlight-js with the default guest

# 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) (check-fixture-lock) (_test-native-modules-unit target) (_test-native-modules-build-guest target) (_test-native-modules-vm target) (_test-native-modules-restore target)
test-native-modules target=default-target: (check-fixture-lock) (_test-native-modules-unit target) (_test-native-modules-manifest 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
_test-native-modules-manifest target=default-target:
{{ set-env-command }}HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH="{{PWD}}/src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml" {{ if os() == "windows" { ";" } else { "&&" } }} cargo test -p hyperlight-js --test native_modules --test runtime_build --profile={{ if target == "debug" {"dev"} else { target } }} -- --include-ignored --nocapture

[private]
_test-native-modules-restore target=default-target:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Provides a capability to run JavaScript inside of Hyperlight using quickjs as th

## Documentation

- [Custom guest runtimes](docs/extending-runtime.md) - Extend with native modules and build and embed a custom guest using cargo-hyperlight

- [Execution Monitors](docs/execution-monitors.md) - Timeout and resource limit enforcement for handler execution
- [Observability](docs/observability.md) - Metrics and tracing
- [Crashdumps](docs/create-and-analyse-guest-crashdumps.md) - Creating and analyzing guest crash dumps
Expand Down
184 changes: 83 additions & 101 deletions docs/extending-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ code that JavaScript handlers can `import` — without forking the runtime.
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.
3. **Build with `cargo hyperlight`** — it discovers Hyperlight's libc
headers and configures the guest compiler and sysroot.
4. **Build and embed with the host** — point `hyperlight-js` at your custom
runtime manifest. Its build script builds the guest and embeds it at
compile time.

Custom guests must use a compatible `hyperlight-js-runtime` and Hyperlight
version with the host library/addon. Pin the guest and host to the same
release (or git revision).

## Quick Start

Expand Down Expand Up @@ -64,47 +70,49 @@ mod math {
hyperlight_js_runtime::native_modules! {
"math" => js_math,
}

hyperlight_js_runtime::custom_globals! {}
```

That's all the Rust you write for the Hyperlight guest. The macro generates
That's the guest application code. 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.
the guest entry point and host function dispatch. Invoke both registration
macros, even when one is empty.

### 3. Build and embed in hyperlight-js

The hyperlight target has no libc, so QuickJS needs stub headers from
`hyperlight-js-runtime/include/` and `-D__wasi__=1` to disable pthreads.
Set `HYPERLIGHT_CFLAGS` before building — the one-liner below uses
`cargo metadata` to resolve the include path from your dependency tree:
**Breaking change:** `HYPERLIGHT_JS_RUNTIME_PATH` is no longer read.
Prebuilt guest binary embedding is no longer supported. Replace that setting
with `HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH` pointing to the custom crate's
`Cargo.toml`, then rebuild the host or Node.js addon from source.
Without a custom manifest, the default runtime is built and embedded, even
if the old variable is still set.

Set `HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH` to the custom crate's **absolute**
`Cargo.toml` path, then build your host project normally:

```bash
# Resolve CFLAGS from hyperlight-js-runtime's include/ directory
export HYPERLIGHT_CFLAGS=$(node -e "
var m=JSON.parse(require('child_process').execSync(
'cargo metadata --format-version 1 --manifest-path my-custom-runtime/Cargo.toml',
{encoding:'utf8',stdio:['pipe','pipe','pipe'],maxBuffer:20*1024*1024}));
var p=m.packages.find(function(p){return p.name==='hyperlight-js-runtime'});
if(p)console.log('-I'+require('path').join(
require('path').dirname(p.manifest_path),'include')+' -D__wasi__=1');
")

# Build the custom runtime for the hyperlight target
cargo hyperlight build --manifest-path my-custom-runtime/Cargo.toml --release

# Tell hyperlight-js to embed the custom runtime (not the default one)
export HYPERLIGHT_JS_RUNTIME_PATH=my-custom-runtime/target/x86_64-hyperlight-none/release/my-custom-runtime

# Rebuild hyperlight-js so the embedded runtime is updated
cargo build -p hyperlight-js --release
export HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH="$(realpath my-custom-runtime/Cargo.toml)"
cargo build --release
```

### 4. Use from the host
PowerShell:

```powershell
$env:HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH = (Resolve-Path .\my-custom-runtime\Cargo.toml).Path
cargo build --release
```

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:
This builds your custom runtime with `cargo-hyperlight` and embeds it in the
host. No additional compiler flags or include paths need to be configured.
The custom runtime manifest must define exactly one binary target.
Re-run the host build after changing your runtime.

### 4. Use from the Rust host

The host-side API is unchanged. Your custom runtime is already embedded,
and handlers simply import your modules:

```rust
use hyperlight_js::{SandboxBuilder, Script};
Expand Down Expand Up @@ -145,7 +153,9 @@ A no-op `Host` is all that's needed — it only gets called for `.js` file
imports, which native modules don't use:

```rust
#[cfg(not(hyperlight))]
struct NoOpHost;
#[cfg(not(hyperlight))]
impl hyperlight_js_runtime::host::Host for NoOpHost {
fn resolve_module(&self, _base: String, name: String) -> anyhow::Result<String> {
anyhow::bail!("Module '{name}' not found")
Expand All @@ -155,6 +165,7 @@ impl hyperlight_js_runtime::host::Host for NoOpHost {
}
}

#[cfg(not(hyperlight))]
fn main() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().collect();
let script = std::fs::read_to_string(&args[1])?;
Expand Down Expand Up @@ -185,95 +196,66 @@ cargo run -- handler.js '{"a":6,"b":7}'
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.
Run `just test-native-modules` to build and embed the fixture.
These VM tests cover custom modules, custom globals, built-ins, and the
host-backed clock. They require a supported hypervisor. Build selection
regressions are also covered by
`cargo test -p hyperlight-js --test runtime_build`.

## Using js-host-api from a Downstream Node.js Project

If your downstream project depends on `@hyperlight/js-host-api` (the
Node.js NAPI addon) and uses a custom runtime, you **cannot** use a
published version of the addon — the published binary has the default
runtime baked in via `include_bytes!()`. You need to build the NAPI
addon from source with your custom runtime embedded.

### Why not just `npm install`?

The `js-host-api` NAPI addon links against the `hyperlight-js` Rust crate,
which embeds the runtime binary at compile time. A published npm package
would contain a `.node` binary with the **default** runtime — your custom
native modules wouldn't be present.
**If you use a custom runtime, you must build the Node.js addon from source
instead of using the published `@hyperlight-dev/js-host-api` binary.**

### The pattern: reuse Cargo's git checkout
### Why the published addon cannot be used

Your custom runtime crate already has a Cargo dependency on
`hyperlight-js-runtime`, which causes Cargo to clone the full
`hyperlight-js` workspace into `~/.cargo/git/checkouts/`. The
`js-host-api` NAPI source is included in that checkout — no separate
git clone needed.
The NAPI addon links against the `hyperlight-js` Rust crate, which embeds
the guest runtime using `include_bytes!()` at compile time. The published
package's `.node` binary therefore already contains the **default** runtime.
Your custom native modules are not in that binary.

#### 1. Discover the checkout path
Running `npm install` to get the published package does not rebuild it with
your guest. Neither building your custom runtime separately nor setting
`HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH` when starting Node.js changes the
runtime inside an already-compiled addon. That variable is read during the
Rust host build, not when JavaScript creates a sandbox.

Use `cargo metadata` to find where Cargo placed the hyperlight-js
workspace:
### Build the addon with your custom runtime

```bash
HYPERLIGHT_DIR=$(node -e "
var m=JSON.parse(require('child_process').execSync(
'cargo metadata --format-version 1 --manifest-path my-custom-runtime/Cargo.toml',
{encoding:'utf8',stdio:['pipe','pipe','pipe'],maxBuffer:20*1024*1024}));
var p=m.packages.find(function(p){return p.name==='hyperlight-js-runtime'});
if(p)console.log(require('path').resolve(
require('path').dirname(p.manifest_path),'..','..'));
")
echo "$HYPERLIGHT_DIR"
# e.g. /home/you/.cargo/git/checkouts/hyperlight-js-abc123/def456
```

#### 2. Build the NAPI addon with your custom runtime
Use a `hyperlight-js` checkout matching the release or git revision used by
your custom runtime. From the checkout root, set the custom manifest's
absolute path and build the addon:

```bash
# Set HYPERLIGHT_CFLAGS for the guest build
export HYPERLIGHT_CFLAGS=$(node -e "
var m=JSON.parse(require('child_process').execSync(
'cargo metadata --format-version 1 --manifest-path my-custom-runtime/Cargo.toml',
{encoding:'utf8',stdio:['pipe','pipe','pipe'],maxBuffer:20*1024*1024}));
var p=m.packages.find(function(p){return p.name==='hyperlight-js-runtime'});
if(p)console.log('-I'+require('path').join(
require('path').dirname(p.manifest_path),'include')+' -D__wasi__=1');
")

# Build your custom runtime for the hyperlight target
cargo hyperlight build --manifest-path my-custom-runtime/Cargo.toml --release

# Point hyperlight-js at your custom runtime binary
export HYPERLIGHT_JS_RUNTIME_PATH=my-custom-runtime/target/x86_64-hyperlight-none/release/my-custom-runtime

# Clean stale builds so build.rs re-embeds the runtime
cd "${HYPERLIGHT_DIR}/src/hyperlight-js" && cargo clean -p hyperlight-js

# Build the NAPI addon from the Cargo checkout
cd "${HYPERLIGHT_DIR}" && just build release
```powershell
$env:HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH = (Resolve-Path C:\path\to\my-custom-runtime\Cargo.toml).Path
just build-js-host-api release
```

#### 3. Symlink for npm dependency resolution
On Bash, use `export HYPERLIGHT_JS_RUNTIME_MANIFEST_PATH=/absolute/path/to/my-custom-runtime/Cargo.toml`
before the same `just` command. This builds and embeds the custom guest as
part of the addon build.

Create a symlink so npm can resolve the addon via a stable path:
### Use the locally built addon

```bash
mkdir -p deps
ln -sfn "${HYPERLIGHT_DIR}/src/js-host-api" deps/js-host-api
```

In your package.json, point to js-host-api via the symlink:
Point your downstream project's npm dependency at the built checkout's
`src/js-host-api` directory, rather than a published version:

```json
{
"dependencies": {
"@hyperlight/js-host-api": "file:deps/js-host-api"
"@hyperlight-dev/js-host-api": "file:../hyperlight-js/src/js-host-api"
}
}
```
Make sure to add `deps` to your `.gitignore` since it's a symlink to a local Cargo checkout.

Adjust the path for your layout, then run `npm install` in the downstream
project to update its dependency and lockfile. The application must use this
locally built addon, not a previously installed published copy.

The JavaScript API is unchanged: use the usual `SandboxBuilder`, and handlers
can import the custom modules embedded in your guest. After changing the
custom runtime, rebuild the addon and refresh the downstream installation
before restarting the application.

## API Reference

Expand Down
9 changes: 8 additions & 1 deletion src/hyperlight-js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_
[build-dependencies]
cargo-hyperlight = "0.1.14"
serde_json = { version = "1.0" }
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
chrono = "0.4.45"
Expand Down Expand Up @@ -82,6 +81,14 @@ monitor-cpu-time = ["dep:libc", "dep:windows-sys"]
[package.metadata.cargo-machete]
ignored = ["hyperlight-js-runtime"]

[lints.clippy]
# lib.rs enables this restriction for release library builds, not tests or tools.
disallowed_macros = "allow"

[[test]]
name = "runtime_build"
path = "runtime_build.rs"

[[example]]
name = "run_handler"
path = "examples/run_handler/main.rs"
Expand Down
1 change: 0 additions & 1 deletion src/hyperlight-js/benches/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ limitations under the License.
*/

// this is benchmarks, assert macros are fine
#![allow(clippy::disallowed_macros)]

use std::time::{Duration, Instant};

Expand Down
Loading