hyperlight_host/sandbox/host_funcs.rs
1/*
2Copyright 2025 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*/
16
17use std::collections::HashMap;
18use std::io::{IsTerminal, Write};
19
20use hyperlight_common::flatbuffer_wrappers::function_types::{
21 ParameterType, ParameterValue, ReturnType, ReturnValue,
22};
23use hyperlight_common::flatbuffer_wrappers::host_function_definition::HostFunctionDefinition;
24use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
25use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
26use tracing::{Span, instrument};
27
28use crate::HyperlightError::HostFunctionNotFound;
29use crate::Result;
30use crate::func::host_functions::TypeErasedHostFunction;
31
32#[derive(Default)]
33/// A Wrapper around details of functions exposed by the Host
34pub struct FunctionRegistry {
35 functions_map: HashMap<String, FunctionEntry>,
36}
37
38/// A collection of host functions that can be supplied to a sandbox
39/// constructor (e.g. [`crate::MultiUseSandbox::from_snapshot`]) to
40/// expose host-side functionality to the guest.
41///
42/// Use [`HostFunctions::default`] to start with the standard
43/// `HostPrint` function pre-registered (matches the registry that the
44/// regular `UninitializedSandbox` → `evolve()` path constructs), or
45/// [`HostFunctions::empty`] to start with an empty registry.
46///
47/// Add additional host functions via the
48/// [`crate::func::Registerable`] trait, just as you would on an
49/// `UninitializedSandbox`.
50///
51/// ```no_run
52/// # use hyperlight_host::{HostFunctions, Result};
53/// # use hyperlight_host::func::Registerable;
54/// # fn example() -> Result<()> {
55/// // Default: HostPrint already registered.
56/// let mut funcs = HostFunctions::default();
57/// funcs.register_host_function("Add", |a: i32, b: i32| Ok(a + b))?;
58/// # Ok(())
59/// # }
60/// ```
61pub struct HostFunctions(FunctionRegistry);
62
63impl HostFunctions {
64 /// Create an empty `HostFunctions` with no host functions
65 /// registered.
66 ///
67 /// Most callers want [`HostFunctions::default`] instead, which
68 /// pre-registers the standard `HostPrint` function. An empty
69 /// registry will fail snapshot validation against any snapshot
70 /// that captured `HostPrint`, and any guest code that tries to
71 /// `printf` into an empty registry will get an EIO from
72 /// `write(2)`.
73 pub fn empty() -> Self {
74 Self(FunctionRegistry::default())
75 }
76
77 /// Consume this `HostFunctions` and return the inner registry.
78 pub(crate) fn into_inner(self) -> FunctionRegistry {
79 self.0
80 }
81
82 /// Borrow the inner registry mutably.
83 pub(crate) fn inner_mut(&mut self) -> &mut FunctionRegistry {
84 &mut self.0
85 }
86
87 /// Borrow the inner registry immutably.
88 pub(crate) fn inner(&self) -> &FunctionRegistry {
89 &self.0
90 }
91}
92
93impl Default for HostFunctions {
94 /// Create a `HostFunctions` pre-populated with the standard
95 /// `HostPrint` function (writes UTF-8 strings to the host's
96 /// stdout in green).
97 ///
98 /// This matches the default registry installed by
99 /// `UninitializedSandbox::new()`, so a snapshot taken from a
100 /// regular sandbox can be loaded with
101 /// `MultiUseSandbox::from_snapshot(snap, HostFunctions::default(), None)`
102 /// without registering anything else.
103 ///
104 /// Use [`HostFunctions::empty`] for an empty registry.
105 fn default() -> Self {
106 Self(FunctionRegistry::with_default_host_print())
107 }
108}
109
110impl From<&FunctionRegistry> for HostFunctionDetails {
111 fn from(registry: &FunctionRegistry) -> Self {
112 let host_functions = registry
113 .functions_map
114 .iter()
115 .map(|(name, entry)| HostFunctionDefinition {
116 function_name: name.clone(),
117 parameter_types: Some(entry.parameter_types.to_vec()),
118 return_type: entry.return_type,
119 })
120 .collect();
121
122 HostFunctionDetails {
123 host_functions: Some(host_functions),
124 }
125 }
126}
127
128pub struct FunctionEntry {
129 pub function: TypeErasedHostFunction,
130 pub parameter_types: &'static [ParameterType],
131 pub return_type: ReturnType,
132}
133
134impl FunctionRegistry {
135 /// Register a host function with the sandbox.
136 #[instrument(skip_all, parent = Span::current(), level = "Trace")]
137 pub(crate) fn register_host_function(&mut self, name: String, func: FunctionEntry) {
138 self.functions_map.insert(name, func);
139 }
140
141 /// Return the registered signature for `name`.
142 pub(crate) fn function_signature(
143 &self,
144 name: &str,
145 ) -> Option<(&'static [ParameterType], ReturnType)> {
146 self.functions_map
147 .get(name)
148 .map(|entry| (entry.parameter_types, entry.return_type))
149 }
150
151 /// Create a `FunctionRegistry` pre-populated with the default
152 /// `HostPrint` function (writes to stdout with green text).
153 pub(crate) fn with_default_host_print() -> Self {
154 use crate::func::host_functions::HostFunction;
155 use crate::func::{ParameterTuple, SupportedReturnType};
156
157 let mut registry = Self::default();
158 let hf: HostFunction<i32, (String,)> = default_writer_func.into();
159 let entry = FunctionEntry {
160 function: hf.into(),
161 parameter_types: <(String,)>::TYPE,
162 return_type: <i32 as SupportedReturnType>::TYPE,
163 };
164 registry.register_host_function("HostPrint".to_string(), entry);
165 registry
166 }
167
168 /// Assuming a host function called `"HostPrint"` exists, and takes a
169 /// single string parameter, call it with the given `msg` parameter.
170 ///
171 /// Return `Ok` if the function was found and was of the right signature,
172 /// and `Err` otherwise.
173 #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
174 #[allow(dead_code)]
175 pub(super) fn host_print(&mut self, msg: String) -> Result<i32> {
176 let res = self.call_host_func_impl("HostPrint", vec![ParameterValue::String(msg)])?;
177 res.try_into()
178 .map_err(|_| HostFunctionNotFound("HostPrint".to_string()))
179 }
180 /// From the set of registered host functions, attempt to get the one
181 /// named `name`. If it exists, call it with the given arguments list
182 /// `args` and return its result.
183 ///
184 /// Return `Err` if no such function exists,
185 /// its parameter list doesn't match `args`, or there was another error
186 /// getting, configuring or calling the function.
187 #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
188 pub(super) fn call_host_function(
189 &self,
190 name: &str,
191 args: Vec<ParameterValue>,
192 ) -> Result<ReturnValue> {
193 self.call_host_func_impl(name, args)
194 }
195
196 #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
197 fn call_host_func_impl(&self, name: &str, args: Vec<ParameterValue>) -> Result<ReturnValue> {
198 let FunctionEntry {
199 function,
200 parameter_types: _,
201 return_type: _,
202 } = self
203 .functions_map
204 .get(name)
205 .ok_or_else(|| HostFunctionNotFound(name.to_string()))?;
206
207 // Make the host function call
208 crate::metrics::maybe_time_and_emit_host_call(name, || function.call(args))
209 }
210}
211
212/// The default writer function is to write to stdout with green text.
213#[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
214fn default_writer_func(s: String) -> Result<i32> {
215 match std::io::stdout().is_terminal() {
216 false => {
217 print!("{}", s);
218 Ok(s.len() as i32)
219 }
220 true => {
221 let mut stdout = StandardStream::stdout(ColorChoice::Auto);
222 let mut color_spec = ColorSpec::new();
223 color_spec.set_fg(Some(Color::Green));
224 stdout.set_color(&color_spec)?;
225 stdout.write_all(s.as_bytes())?;
226 stdout.reset()?;
227 Ok(s.len() as i32)
228 }
229 }
230}