Skip to main content

hyperlight_host/func/
host_functions.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4use std::sync::{Arc, Mutex};
5
6use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterValue, ReturnValue};
7use hyperlight_common::for_each_tuple;
8use hyperlight_common::func::{Error as FuncError, Function, ResultType};
9
10use super::{ParameterTuple, SupportedReturnType};
11use crate::sandbox::UninitializedSandbox;
12use crate::sandbox::host_funcs::FunctionEntry;
13use crate::{HyperlightError, Result, new_error};
14
15/// A sandbox on which (primitive) host functions can be registered
16///
17pub trait Registerable {
18    /// Register a primitive host function
19    fn register_host_function<Args: ParameterTuple, Output: SupportedReturnType>(
20        &mut self,
21        name: &str,
22        hf: impl Into<HostFunction<Output, Args>>,
23    ) -> Result<()>;
24}
25impl Registerable for UninitializedSandbox {
26    fn register_host_function<Args: ParameterTuple, Output: SupportedReturnType>(
27        &mut self,
28        name: &str,
29        hf: impl Into<HostFunction<Output, Args>>,
30    ) -> Result<()> {
31        let mut hfs = self
32            .host_funcs
33            .try_lock()
34            .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
35
36        let entry = FunctionEntry {
37            function: hf.into().into(),
38            parameter_types: Args::TYPE,
39            return_type: Output::TYPE,
40        };
41
42        (*hfs).register_host_function(name.to_string(), entry);
43        Ok(())
44    }
45}
46
47/// Allow registering host functions on an already-evolved
48/// [`crate::MultiUseSandbox`].
49///
50/// The primary entry point for host-function registration is
51/// [`crate::SandboxBuilder::host_function`] — that's the lifecycle
52/// phase where the guest hasn't yet been allowed to issue host calls.
53/// There are, however, cases where a `MultiUseSandbox` is obtained
54/// without going through the builder:
55///
56/// - Sandboxes loaded from a persisted snapshot.
57/// - Any future API that yields a `MultiUseSandbox` directly.
58///
59/// In those cases the caller never had a chance to register up front,
60/// so we expose the same trait implementation here for late
61/// registration.
62/// The guest's host-function dispatcher resolves by name at call
63/// time, so inserting into the registry after the sandbox is built is
64/// semantically safe as long as the first host-function invocation
65/// happens after registration completes.
66impl Registerable for crate::MultiUseSandbox {
67    fn register_host_function<Args: ParameterTuple, Output: SupportedReturnType>(
68        &mut self,
69        name: &str,
70        hf: impl Into<HostFunction<Output, Args>>,
71    ) -> Result<()> {
72        let mut hfs = self
73            .host_funcs
74            .try_lock()
75            .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
76
77        let entry = FunctionEntry {
78            function: hf.into().into(),
79            parameter_types: Args::TYPE,
80            return_type: Output::TYPE,
81        };
82
83        (*hfs).register_host_function(name.to_string(), entry);
84
85        // Registration mutates the host-function set captured in
86        // snapshots. Invalidate the cached snapshot so the next
87        // `snapshot()` call reflects the updated registry.
88        self.snapshot = None;
89        Ok(())
90    }
91}
92
93impl Registerable for crate::HostFunctions {
94    fn register_host_function<Args: ParameterTuple, Output: SupportedReturnType>(
95        &mut self,
96        name: &str,
97        hf: impl Into<HostFunction<Output, Args>>,
98    ) -> Result<()> {
99        let entry = FunctionEntry {
100            function: hf.into().into(),
101            parameter_types: Args::TYPE,
102            return_type: Output::TYPE,
103        };
104
105        self.inner_mut()
106            .register_host_function(name.to_string(), entry);
107        Ok(())
108    }
109}
110
111/// A representation of a host function.
112/// This is a thin wrapper around a `Fn(Args) -> Result<Output>`.
113#[derive(Clone)]
114pub struct HostFunction<Output, Args>
115where
116    Args: ParameterTuple,
117    Output: SupportedReturnType,
118{
119    // This is a thin wrapper around a `Function<Output, Args, HyperlightError>`.
120    // But unlike `Function<..>` which is a trait, this is a concrete type.
121    // This allows us to:
122    //  1. Impose constraints on the function arguments and return type.
123    //  2. Impose a single function signature.
124    //
125    // This second point is important because the `Function<..>` trait is generic
126    // over the function arguments and return type.
127    // This means that a given type could implement `Function<..>` for multiple
128    // function signatures.
129    // This means we can't do something like:
130    // ```rust,ignore
131    // impl<Args, Output, F> SomeTrait for F
132    // where
133    //     F: Function<Output, Args, HyperlightError>,
134    // { ... }
135    // ```
136    // because the concrete type F might implement `Function<..>` for multiple
137    // arguments and return types, and that would means implementing `SomeTrait`
138    // multiple times for the same type F.
139
140    // Use Arc in here instead of Box because it's useful in tests and
141    // presumably in other places to be able to clone a HostFunction and
142    // use it across different sandboxes.
143    func: Arc<dyn Function<Output, Args, HyperlightError> + Send + Sync + 'static>,
144}
145
146pub(crate) struct TypeErasedHostFunction {
147    func: Box<dyn Fn(Vec<ParameterValue>) -> Result<ReturnValue> + Send + Sync + 'static>,
148}
149
150impl<Args, Output> HostFunction<Output, Args>
151where
152    Args: ParameterTuple,
153    Output: SupportedReturnType,
154{
155    /// Call the host function with the given arguments.
156    pub fn call(&self, args: Args) -> Result<Output> {
157        self.func.call(args)
158    }
159}
160
161impl TypeErasedHostFunction {
162    pub(crate) fn call(&self, args: Vec<ParameterValue>) -> Result<ReturnValue> {
163        (self.func)(args)
164    }
165}
166
167impl From<FuncError> for HyperlightError {
168    fn from(e: FuncError) -> Self {
169        match e {
170            FuncError::ParameterValueConversionFailure(from, to) => {
171                HyperlightError::ParameterValueConversionFailure(from, to)
172            }
173            FuncError::ReturnValueConversionFailure(from, to) => {
174                HyperlightError::ReturnValueConversionFailure(from, to)
175            }
176            FuncError::UnexpectedNoOfArguments(got, expected) => {
177                HyperlightError::UnexpectedNoOfArguments(got, expected)
178            }
179            FuncError::UnexpectedParameterValueType(got, expected) => {
180                HyperlightError::UnexpectedParameterValueType(got, expected)
181            }
182            FuncError::UnexpectedReturnValueType(got, expected) => {
183                HyperlightError::UnexpectedReturnValueType(got, expected)
184            }
185        }
186    }
187}
188
189impl<Args, Output> From<HostFunction<Output, Args>> for TypeErasedHostFunction
190where
191    Args: ParameterTuple,
192    Output: SupportedReturnType,
193{
194    fn from(func: HostFunction<Output, Args>) -> TypeErasedHostFunction {
195        TypeErasedHostFunction {
196            func: Box::new(move |args: Vec<ParameterValue>| {
197                let args = Args::from_value(args)?;
198                Ok(func.call(args)?.into_value())
199            }),
200        }
201    }
202}
203
204macro_rules! impl_host_function {
205    ([$N:expr] ($($p:ident: $P:ident),*)) => {
206        /*
207        // Normally for a `Fn + Send + Sync` we don't need to use a Mutex
208        // like we do in the case of a `FnMut`.
209        // However, we can't implement `IntoHostFunction` for `Fn` and `FnMut`
210        // because `FnMut` is a supertrait of `Fn`.
211         */
212
213        impl<F, R, $($P),*> From<F> for HostFunction<R::ReturnType, ($($P,)*)>
214        where
215            F: FnMut($($P),*) -> R + Send + 'static,
216            ($($P,)*): ParameterTuple,
217            R: ResultType<HyperlightError>,
218        {
219            fn from(func: F) -> HostFunction<R::ReturnType, ($($P,)*)> {
220                let func = Mutex::new(func);
221                let func = move |$($p: $P,)*| {
222                    let mut func = func.lock().map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
223                    (func)($($p),*).into_result()
224                };
225                let func = Arc::new(func);
226                HostFunction { func }
227            }
228        }
229    };
230}
231
232for_each_tuple!(impl_host_function);
233
234pub(crate) fn register_host_function<Args: ParameterTuple, Output: SupportedReturnType>(
235    func: impl Into<HostFunction<Output, Args>>,
236    sandbox: &mut UninitializedSandbox,
237    name: &str,
238) -> Result<()> {
239    let func = func.into().into();
240
241    let entry = FunctionEntry {
242        function: func,
243        parameter_types: Args::TYPE,
244        return_type: Output::TYPE,
245    };
246
247    sandbox
248        .host_funcs
249        .try_lock()
250        .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?
251        .register_host_function(name.to_string(), entry);
252
253    Ok(())
254}