Skip to main content

hyperlight_js/sandbox/
proto_js_sandbox.rs

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