feat: allow extending hyperlight-js-runtime with custom native modules#49
feat: allow extending hyperlight-js-runtime with custom native modules#49simongdavies wants to merge 6 commits intohyperlight-dev:mainfrom
Conversation
d721317 to
3e43831
Compare
7dcc0a9 to
2741c45
Compare
There was a problem hiding this comment.
Pull request overview
Adds an extension mechanism to hyperlight-js-runtime so downstream crates can register custom native Rust modules and custom globals (and optionally override built-ins) without forking, and wires host-side embedding to support a custom runtime binary via HYPERLIGHT_JS_RUNTIME_PATH.
Changes:
- Introduces a custom native-module registry +
native_modules!macro and acustom_globals!macro, and updatesJsRuntime::new()init sequence to: built-in globals → custom globals → freeze. - Adds host build-time support for embedding a custom runtime binary (
HYPERLIGHT_JS_RUNTIME_PATH) and ajust test-native-modulespipeline to exercise it. - Adds fixtures, docs, and tests covering custom modules/globals, console extensibility, and post-init freezing behavior.
Reviewed changes
Copilot reviewed 19 out of 25 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/hyperlight-js/tests/native_modules.rs | Ignored VM integration tests validating custom modules/globals via embedded custom runtime. |
| src/hyperlight-js/build.rs | Adds HYPERLIGHT_JS_RUNTIME_PATH override for embedding a custom runtime binary. |
| src/hyperlight-js-runtime/tests/native_modules.rs | Unit/E2E tests for registry, macros, globals setup, freeze behavior, and fixture pipeline. |
| src/hyperlight-js-runtime/tests/fixtures/native_math/src/lib.rs | Fixture native module providing add/multiply. |
| src/hyperlight-js-runtime/tests/fixtures/native_math/Cargo.toml | Fixture crate manifest for native-math. |
| src/hyperlight-js-runtime/tests/fixtures/extended_runtime/src/main.rs | Fixture binary using native_modules! and custom_globals! to extend the runtime. |
| src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.toml | Fixture binary manifest depending on the runtime lib + fixture module. |
| src/hyperlight-js-runtime/tests/fixtures/extended_runtime/Cargo.lock | Lockfile for the standalone fixture build. |
| src/hyperlight-js-runtime/src/modules/mod.rs | Implements custom module registry, lazy init symbol, unified loader, and both extension macros. |
| src/hyperlight-js-runtime/src/main.rs | Provides empty native_modules!{} / custom_globals!{} symbols for the upstream binary. |
| src/hyperlight-js-runtime/src/lib.rs | Exposes runtime as a library and adds 3-step init (setup → custom globals → freeze). |
| src/hyperlight-js-runtime/src/guest/stubs/srand.rs | Adds libc stub for srand. |
| src/hyperlight-js-runtime/src/guest/stubs/mod.rs | Registers the new/relocated libc stub modules. |
| src/hyperlight-js-runtime/src/guest/stubs/localtime.rs | Adds libc stub implementation for localtime_r. |
| src/hyperlight-js-runtime/src/guest/stubs/io.rs | Adds libc stubs for putchar/fflush. |
| src/hyperlight-js-runtime/src/guest/stubs/clock.rs | Adds libc stubs for clock_gettime/gettimeofday using host time. |
| src/hyperlight-js-runtime/src/guest/mod.rs | Moves guest entrypoint/plumbing into the library (cfg(hyperlight)). |
| src/hyperlight-js-runtime/src/globals/print.rs | Makes print writable during custom-globals phase (frozen later). |
| src/hyperlight-js-runtime/src/globals/mod.rs | Adds freeze() stage API and wires in new freeze module. |
| src/hyperlight-js-runtime/src/globals/freeze.rs | New freeze stage: freezes console object and locks down print. |
| src/hyperlight-js-runtime/src/globals/console.rs | Installs console as an extensible object (not module namespace) before freezing. |
| src/hyperlight-js-runtime/Cargo.toml | Adds [lib] target and makes the bin explicitly point to src/main.rs. |
| docs/extending-runtime.md | Adds documentation for extending runtime with custom modules/globals and embedding workflow. |
| Justfile | Adds test-native-modules pipeline and updates clean/test recipes accordingly. |
| Cargo.toml | Excludes the extended runtime fixture from the workspace. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Resolves hyperlight-dev#48 Add a registration-based system for extending the JS runtime with custom native (Rust-implemented) modules that run inside the Hyperlight guest VM. Key changes: - hyperlight-js-runtime/Cargo.toml: Add [lib] target so the runtime can be used as a library dependency by extender crates. - modules/mod.rs: Add global CUSTOM_MODULES registry with register_native_module() and builtin_module_names(). NativeModuleLoader checks custom registry first, falls back to built-in modules. Panics if custom module name conflicts with a built-in. - native_modules! macro: Generates init_native_modules() (#[no_mangle]) that registers custom modules. Called automatically via spin::Once on first NativeModuleLoader access — no explicit init needed. - guest/mod.rs: Move hyperlight guest entry point (hyperlight_main, guest_dispatch_function, Host impl, stubs) from main/hyperlight.rs into the lib behind cfg(hyperlight). Extender binaries get all guest infrastructure for free by depending on the lib. - hyperlight-js/build.rs: Add HYPERLIGHT_JS_RUNTIME_PATH env var override to embed custom runtime binaries instead of the default. - host.rs: Restore original Host trait only (no FsHost extraction). Testing: - 13 unit/integration tests in hyperlight-js-runtime (loader, registry, macro, override prevention, E2E with native CLI fixture binary) - 3 Hyperlight VM integration tests in hyperlight-js (#[ignore], run via just test-native-modules) - Extended runtime fixture crate with shared native_math module - just test-native-modules recipe for full hyperlight pipeline Docs: - docs/extending-runtime.md with quick start, host-side usage, native testing, API reference, and architecture diagram
Add custom_globals! macro alongside native_modules! to allow extender
crates to register global JS objects (TextEncoder, TextDecoder,
polyfills, constants) without modifying hyperlight-js-runtime.
- New custom_globals! macro in modules/mod.rs (same #[no_mangle] + extern
- setup_custom_globals() bridge called in JsRuntime::new() after
built-in globals
- Default empty custom_globals! {} in base runtime main.rs
- Test fixture with globalThis.CUSTOM_GLOBAL_TEST = 42
- 6 new tests: unit e2e + full pipeline (standalone, coexist with
builtins, combined with native modules)
- Documentation in docs/extending-runtime.md
- Allow overriding built-in modules (io, crypto, console) via native_modules! — custom modules take priority over built-ins. The require module is protected and cannot be overridden. - Make console and print globals writable during init so custom_globals! can extend them (e.g. add console.warn/error). - Freeze console (Object.freeze) and print (non-writable) after custom_globals! runs — handler code cannot tamper with them. - 3-step init in JsRuntime::new: setup → custom_globals → freeze. - New globals/freeze.rs module for post-init lockdown. - Tests: require override rejection, console extension via custom_globals, freeze verification, console.log after freeze. - Updated docs/extending-runtime.md with override rules. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
75c23b9 to
48a251a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 25 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Copy fn pointer out of CUSTOM_MODULES lock before calling module declaration to prevent potential deadlock (spin::Mutex not re-entrant) - Move cargo:rerun-if-env-changed=HYPERLIGHT_JS_RUNTIME_PATH before the match so Cargo tracks the env var even when unset; use canonical path for rerun-if-changed - Lock down globalThis.console binding (non-writable/non-configurable) after Object.freeze to prevent handler code replacing it entirely - Fix test doc comment: built-in overrides work except for require Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
48a251a to
5915fb7
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 25 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| [workspace] | ||
| resolver = "2" | ||
| members = ["src/hyperlight-js", "src/js-host-api", "src/hyperlight-js-runtime"] | ||
| exclude = ["src/hyperlight-js-runtime/tests/fixtures/extended_runtime"] |
There was a problem hiding this comment.
The workspace exclude list only excludes the extended_runtime fixture, but tests/fixtures/native_math/Cargo.toml also declares its own [workspace]. If this is meant to avoid nested-workspace conflicts, it’s safer to exclude the whole fixtures directory (or at least both extended_runtime and native_math) to prevent Cargo from treating nested workspace roots as part of the parent workspace in some commands.
| exclude = ["src/hyperlight-js-runtime/tests/fixtures/extended_runtime"] | |
| exclude = ["src/hyperlight-js-runtime/tests/fixtures"] |
| // 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. | ||
|
|
There was a problem hiding this comment.
The hyperlight guest entrypoints (e.g. hyperlight_main, guest_dispatch_function) now live in the library crate, but this binary doesn’t reference them. Because an rlib is an archive, the linker may omit the guest module’s object/codegen units if nothing pulls them in, producing a guest binary missing the required exported symbols. Consider forcing the guest module to be linked (e.g., by referencing a #[cfg(hyperlight)] symbol from guest via a #[used] static, or otherwise ensuring the guest module is part of the final link).
| #[cfg(hyperlight)] | |
| extern "C" { | |
| /// Guest entrypoint provided by the library crate's `guest` module. | |
| /// | |
| /// This declaration is only used to force the guest module to be linked | |
| /// into the final hyperlight binary; it is not invoked from this file. | |
| fn hyperlight_main(); | |
| } | |
| #[cfg(hyperlight)] | |
| #[used] | |
| static _FORCE_GUEST_LINK: unsafe extern "C" fn() = hyperlight_main; |
| } | ||
|
|
||
| // For hyperlight builds: the lib's `guest` module provides hyperlight_main, | ||
| // guest_dispatch_function, and all plumbing. Nothing else needed here. |
There was a problem hiding this comment.
This fixture binary relies on the runtime library to provide Hyperlight guest entrypoints, but it doesn’t reference any of those symbols. With static linking of rlibs, the guest module can be dropped if not pulled in by an undefined reference, resulting in a guest binary that builds but lacks hyperlight_main/dispatch symbols at runtime. Please ensure the fixture (and recommended downstream pattern) forces the guest module to be linked into the final artifact.
| // guest_dispatch_function, and all plumbing. Nothing else needed here. | |
| // guest_dispatch_function, and all plumbing. Nothing else needed here. | |
| // Force the guest module to be linked for hyperlight builds by creating | |
| // undefined references to the guest entrypoints provided by the runtime. | |
| #[cfg(hyperlight)] | |
| extern "C" { | |
| fn hyperlight_main(); | |
| fn guest_dispatch_function(); | |
| } | |
| /// Linker anchor to ensure Hyperlight guest entrypoints are retained. | |
| /// | |
| /// This function is never called; it simply references the extern symbols | |
| /// so that the linker must resolve them from the runtime crate. | |
| #[cfg(hyperlight)] | |
| #[no_mangle] | |
| pub extern "C" fn _hyperlight_guest_link_anchor() { | |
| unsafe { | |
| let _ = hyperlight_main as unsafe extern "C" fn(); | |
| let _ = guest_dispatch_function as unsafe extern "C" fn(); | |
| } | |
| } |
Resolves #48
Overview
Adds an extension system that lets downstream crates add custom native modules, custom globals, and override built-in modules — without forking the runtime.
Architecture
Three macros, one init sequence:
native_modules!— Custom native modulesRegister Rust-implemented modules that guest JS can
import:io,crypto,consolecan be overriddenrequirecannot be overridden (panics — it's core infrastructure)#[no_mangle]+extern "Rust"linkagecustom_globals!— Custom global objectsRegister global objects (constructors, polyfills, constants) without import:
console(addwarn,error,info,debug)#[rquickjs::class]) and JS polyfills (ctx.eval()) supportedGlobals freeze
After
custom_globals!runs, built-in globals are locked down:console→Object.freeze()(no new properties, no modifications)print→ non-writable, non-configurablerequire→ already non-configurable from setup viaProperty::from()Handler code cannot tamper with any of these.
Runtime as a library
hyperlight-js-runtimenow exposes a[lib]target. Extender crates depend on it and provide a binary that links everything together. TheHYPERLIGHT_JS_RUNTIME_PATHbuild-time env var tellshyperlight-jsto embed the custom binary.Guest infrastructure (entry point, host function dispatch, libc stubs) moved from
src/main/tosrc/guest/and is provided by the lib — no copying needed.Key changes
src/hyperlight-js-runtime/src/modules/mod.rsNativeModuleLoader,register_native_module,native_modules!,custom_globals!,setup_custom_globals()src/hyperlight-js-runtime/src/lib.rssetup→custom_globals→freezesrc/hyperlight-js-runtime/src/globals/console.rssrc/hyperlight-js-runtime/src/globals/print.rssrc/hyperlight-js-runtime/src/globals/freeze.rssrc/hyperlight-js-runtime/src/globals/mod.rsfreeze()public functionsrc/hyperlight-js-runtime/src/main.rsnative_modules! {}+custom_globals! {}src/hyperlight-js-runtime/src/guest/src/main/hyperlight.rs— lib-provided guest infrasrc/hyperlight-js/build.rsHYPERLIGHT_JS_RUNTIME_PATHsupportdocs/extending-runtime.mdTests
tests/native_modules.rs: loader resolution, custom modules, built-in override,requireprotection,custom_globals!macro, console extensions, freeze behavioursrc/hyperlight-js/tests/native_modules.rs:HYPERLIGHT_JS_RUNTIME_PATHembeddingnative_math(shared lib) +extended_runtime(binary with custom modules + globals)