Skip to main content

hyperlight_js/sandbox/
proto_js_sandbox.rs

1/*
2Copyright 2026  The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16use std::collections::HashMap;
17use std::fmt::Debug;
18use std::time::SystemTime;
19
20use anyhow::Context;
21use hyperlight_host::sandbox::SandboxConfiguration;
22use hyperlight_host::{new_error, GuestBinary, Result, UninitializedSandbox};
23use serde::de::DeserializeOwned;
24use serde::Serialize;
25use tracing::{instrument, Level};
26
27use super::js_sandbox::JSSandbox;
28use super::sandbox_builder::SandboxBuilder;
29use crate::sandbox::host_fn::{Function, HostModule};
30use crate::sandbox::metrics::SandboxMetricsGuard;
31use crate::HostPrintFn;
32
33/// A Hyperlight Sandbox with no JavaScript run time loaded and no guest code.
34/// This is used to register new host functions prior to loading the JavaScript run time.
35pub struct ProtoJSSandbox {
36    inner: UninitializedSandbox,
37    host_modules: HashMap<String, HostModule>,
38    // metric drop guard to manage sandbox metric
39    _metric_guard: SandboxMetricsGuard<ProtoJSSandbox>,
40}
41
42impl ProtoJSSandbox {
43    #[instrument(err(Debug), skip_all, level=Level::INFO, fields(version= env!("CARGO_PKG_VERSION")))]
44    pub(super) fn new(
45        guest_binary: GuestBinary,
46        cfg: Option<SandboxConfiguration>,
47        host_print_writer: Option<HostPrintFn>,
48    ) -> Result<Self> {
49        let mut usbox: UninitializedSandbox = UninitializedSandbox::new(guest_binary, cfg)?;
50
51        // Set the host print function
52        if let Some(host_print_writer) = host_print_writer {
53            usbox.register_print(host_print_writer)?;
54        }
55
56        // host function used by rquickjs for Date.now()
57        fn current_time_micros() -> hyperlight_host::Result<u64> {
58            Ok(SystemTime::now()
59                .duration_since(SystemTime::UNIX_EPOCH)
60                .with_context(|| "Unable to get duration since epoch")
61                .map(|d| d.as_micros() as u64)?)
62        }
63
64        usbox.register("CurrentTimeMicros", current_time_micros)?;
65
66        Ok(Self {
67            inner: usbox,
68            host_modules: HashMap::new(),
69            _metric_guard: SandboxMetricsGuard::new(),
70        })
71    }
72
73    /// Install a custom file system for module resolution and loading.
74    ///
75    /// Enables JavaScript module imports using the provided ~FileSystem~ implementation.
76    #[instrument(err(Debug), skip_all, level=Level::INFO)]
77    pub fn set_module_loader<Fs: crate::resolver::FileSystem + Clone + 'static>(
78        mut self,
79        file_system: Fs,
80    ) -> Result<Self> {
81        use std::path::PathBuf;
82
83        use oxc_resolver::{ResolveOptions, ResolverGeneric};
84
85        let resolver = ResolverGeneric::new_with_file_system(
86            file_system.clone(),
87            ResolveOptions {
88                extensions: vec![".js".into(), ".mjs".into()],
89                condition_names: vec!["import".into(), "module".into()],
90                ..Default::default()
91            },
92        );
93
94        self.inner.register(
95            "ResolveModule",
96            move |base: String, specifier: String| -> hyperlight_host::Result<String> {
97                tracing::debug!(
98                    base = %base,
99                    specifier = %specifier,
100                    "Resolving module"
101                );
102
103                let resolved = resolver.resolve(&base, &specifier).map_err(|e| {
104                    new_error!(
105                        "Failed to resolve module '{}' from '{}': {:?}",
106                        specifier,
107                        base,
108                        e
109                    )
110                })?;
111
112                Ok(resolved.path().to_string_lossy().to_string())
113            },
114        )?;
115
116        self.inner.register(
117            "LoadModule",
118            move |path: String| -> hyperlight_host::Result<String> {
119                tracing::debug!(path = %path, "Loading module");
120                let path_buf = PathBuf::from(&path);
121                let source = file_system
122                    .read_to_string(&path_buf)
123                    .map_err(|e| new_error!("Failed to read module '{}': {}", path, e))?;
124
125                Ok(source)
126            },
127        )?;
128
129        Ok(self)
130    }
131
132    /// Load the JavaScript runtime into the sandbox.
133    #[instrument(err(Debug), skip(self), level=Level::INFO)]
134    pub fn load_runtime(mut self) -> Result<JSSandbox> {
135        let host_modules = self.host_modules;
136
137        let host_modules_json = serde_json::to_string(&host_modules)?;
138
139        self.inner.register(
140            "CallHostJsFunction",
141            move |module_name: String, func_name: String, args: String| -> Result<String> {
142                let module = host_modules
143                    .get(&module_name)
144                    .ok_or_else(|| new_error!("Host module '{}' not found", module_name))?;
145                let func = module.get(&func_name).ok_or_else(|| {
146                    new_error!(
147                        "Host function '{}' not found in module '{}'",
148                        func_name,
149                        module_name
150                    )
151                })?;
152                func(args)
153            },
154        )?;
155
156        let mut multi_use_sandbox = self.inner.evolve()?;
157
158        let _: () = multi_use_sandbox.call("RegisterHostModules", host_modules_json)?;
159
160        JSSandbox::new(multi_use_sandbox)
161    }
162
163    /// Register a host module that can be called from the guest JavaScript code.
164    ///
165    /// This method should be called **before** [`ProtoJSSandbox::load_runtime`], while
166    /// the sandbox is still in its "proto" (uninitialized) state. After
167    /// [`load_runtime`](Self::load_runtime) is called, the set of host modules and
168    /// functions is fixed for the resulting [`JSSandbox`].
169    ///
170    /// Calling this method multiple times with the same `name` refers to the same
171    /// module; additional calls will reuse the existing module instance and allow
172    /// you to register more functions on it. The first call creates the module and
173    /// subsequent calls return the previously created module.
174    ///
175    /// Module names are matched by exact string equality from the guest
176    /// JavaScript environment. They should be valid UTF‑8 strings and while there is
177    /// no explicit restriction on special characters, using simple, ASCII identifiers
178    /// (e.g. `"fs"`, `"net"`, `"my_module"`) is recommended for portability and clarity.
179    ///
180    /// # Example
181    ///
182    /// ```
183    /// use hyperlight_js::SandboxBuilder;
184    ///
185    /// // Create a proto sandbox and register a host function.
186    /// let mut sbox = SandboxBuilder::new().build()?;
187    ///
188    /// // Register a module and a function on it before loading the runtime.
189    /// sbox.host_module("math").register("add", |a: i32, b: i32| a + b);
190    ///
191    /// // Once all host modules/functions are registered, load the JS runtime.
192    /// let js_sandbox = sbox.load_runtime()?;
193    /// # Ok::<(), hyperlight_host::HyperlightError>(())
194    /// ```
195    #[instrument(skip(self), level=Level::INFO)]
196    pub fn host_module(&mut self, name: impl Into<String> + Debug) -> &mut HostModule {
197        self.host_modules.entry(name.into()).or_default()
198    }
199
200    /// Register a host function that can be called from the guest JavaScript code.
201    /// This is equivalent to calling `sbox.host_module(module).register(name, func)`.
202    ///
203    /// Registering a function with the same `module` and `name` as an existing function
204    /// overwrites the previous registration.
205    #[instrument(err(Debug), skip(self, func), level=Level::INFO)]
206    pub fn register<Output: Serialize, Args: DeserializeOwned>(
207        &mut self,
208        module: impl Into<String> + Debug,
209        name: impl Into<String> + Debug,
210        func: impl Function<Output, Args> + Send + Sync + 'static,
211    ) -> Result<()> {
212        self.host_module(module).register(name, func);
213        Ok(())
214    }
215
216    /// Register a raw host function that operates on JSON strings directly.
217    ///
218    /// This is equivalent to calling `sbox.host_module(module).register_raw(name, func)`.
219    ///
220    /// Unlike [`register`](Self::register), which handles serde serialization /
221    /// deserialization automatically, this method passes the raw JSON string
222    /// from the guest to the callback and expects a JSON string result.
223    ///
224    /// Primarily intended for dynamic / bridge scenarios (e.g. NAPI bindings)
225    /// where argument types are not known at compile time.
226    #[instrument(err(Debug), skip(self, func), level=Level::INFO)]
227    pub fn register_raw(
228        &mut self,
229        module: impl Into<String> + Debug,
230        name: impl Into<String> + Debug,
231        func: impl Fn(String) -> Result<String> + Send + Sync + 'static,
232    ) -> Result<()> {
233        self.host_module(module).register_raw(name, func);
234        Ok(())
235    }
236}
237
238impl std::fmt::Debug for ProtoJSSandbox {
239    #[instrument(skip_all, level=Level::TRACE)]
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        f.debug_struct("ProtoJsSandbox").finish()
242    }
243}
244
245impl Default for ProtoJSSandbox {
246    #[instrument(skip_all, level=Level::INFO)]
247    fn default() -> Self {
248        // This should not fail so we unwrap it.
249        // If it does fail then it is a fundamental bug.
250        #[allow(clippy::unwrap_used)]
251        SandboxBuilder::new().build().unwrap()
252    }
253}