hyperlight_guest/
error.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 alloc::format;
18use alloc::string::{String, ToString as _};
19
20use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
21use hyperlight_common::func::Error as FuncError;
22use {anyhow, serde_json};
23
24pub type Result<T> = core::result::Result<T, HyperlightGuestError>;
25
26#[derive(Debug)]
27pub struct HyperlightGuestError {
28    pub kind: ErrorCode,
29    pub message: String,
30}
31
32impl HyperlightGuestError {
33    pub fn new(kind: ErrorCode, message: String) -> Self {
34        Self { kind, message }
35    }
36}
37
38impl From<anyhow::Error> for HyperlightGuestError {
39    fn from(error: anyhow::Error) -> Self {
40        Self {
41            kind: ErrorCode::GuestError,
42            message: format!("Error: {:?}", error),
43        }
44    }
45}
46
47impl From<serde_json::Error> for HyperlightGuestError {
48    fn from(error: serde_json::Error) -> Self {
49        Self {
50            kind: ErrorCode::GuestError,
51            message: format!("Error: {:?}", error),
52        }
53    }
54}
55
56impl From<FuncError> for HyperlightGuestError {
57    fn from(e: FuncError) -> Self {
58        match e {
59            FuncError::ParameterValueConversionFailure(..) => HyperlightGuestError::new(
60                ErrorCode::GuestFunctionParameterTypeMismatch,
61                e.to_string(),
62            ),
63            FuncError::ReturnValueConversionFailure(..) => HyperlightGuestError::new(
64                ErrorCode::GuestFunctionParameterTypeMismatch,
65                e.to_string(),
66            ),
67            FuncError::UnexpectedNoOfArguments(..) => HyperlightGuestError::new(
68                ErrorCode::GuestFunctionIncorrecNoOfParameters,
69                e.to_string(),
70            ),
71            FuncError::UnexpectedParameterValueType(..) => HyperlightGuestError::new(
72                ErrorCode::GuestFunctionParameterTypeMismatch,
73                e.to_string(),
74            ),
75            FuncError::UnexpectedReturnValueType(..) => HyperlightGuestError::new(
76                ErrorCode::GuestFunctionParameterTypeMismatch,
77                e.to_string(),
78            ),
79        }
80    }
81}
82
83/// Extension trait to add context to `Option<T>` and `Result<T, E>` types in guest code,
84/// converting them to `Result<T, HyperlightGuestError>`.
85///
86/// This is similar to anyhow::Context.
87pub trait GuestErrorContext {
88    type Ok;
89    /// Adds context to the error if `self` is `None` or `Err`.
90    fn context(self, ctx: impl Into<String>) -> Result<Self::Ok>;
91    /// Adds context and a specific error code to the error if `self` is `None` or `Err`.
92    fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<Self::Ok>;
93    /// Lazily adds context to the error if `self` is `None` or `Err`.
94    ///
95    /// This is useful if constructing the context message is expensive.
96    fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<Self::Ok>;
97    /// Lazily adds context and a specific error code to the error if `self` is `None` or `Err`.
98    ///
99    /// This is useful if constructing the context message is expensive.
100    fn with_context_and_code<S: Into<String>>(
101        self,
102        ec: ErrorCode,
103        ctx: impl FnOnce() -> S,
104    ) -> Result<Self::Ok>;
105}
106
107impl<T> GuestErrorContext for Option<T> {
108    type Ok = T;
109    #[inline]
110    fn context(self, ctx: impl Into<String>) -> Result<T> {
111        self.with_context_and_code(ErrorCode::GuestError, || ctx)
112    }
113    #[inline]
114    fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<T> {
115        self.with_context_and_code(ec, || ctx)
116    }
117    #[inline]
118    fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<T> {
119        self.with_context_and_code(ErrorCode::GuestError, ctx)
120    }
121    #[inline]
122    fn with_context_and_code<S: Into<String>>(
123        self,
124        ec: ErrorCode,
125        ctx: impl FnOnce() -> S,
126    ) -> Result<Self::Ok> {
127        match self {
128            Some(s) => Ok(s),
129            None => Err(HyperlightGuestError::new(ec, ctx().into())),
130        }
131    }
132}
133
134impl<T, E: core::fmt::Debug> GuestErrorContext for core::result::Result<T, E> {
135    type Ok = T;
136    #[inline]
137    fn context(self, ctx: impl Into<String>) -> Result<T> {
138        self.with_context_and_code(ErrorCode::GuestError, || ctx)
139    }
140    #[inline]
141    fn context_and_code(self, ec: ErrorCode, ctx: impl Into<String>) -> Result<T> {
142        self.with_context_and_code(ec, || ctx)
143    }
144    #[inline]
145    fn with_context<S: Into<String>>(self, ctx: impl FnOnce() -> S) -> Result<T> {
146        self.with_context_and_code(ErrorCode::GuestError, ctx)
147    }
148    #[inline]
149    fn with_context_and_code<S: Into<String>>(
150        self,
151        ec: ErrorCode,
152        ctx: impl FnOnce() -> S,
153    ) -> Result<T> {
154        match self {
155            Ok(s) => Ok(s),
156            Err(e) => Err(HyperlightGuestError::new(
157                ec,
158                format!("{}.\nCaused by: {e:?}", ctx().into()),
159            )),
160        }
161    }
162}
163
164/// Macro to return early with a `Err(HyperlightGuestError)`.
165/// Usage:
166/// ```ignore
167/// bail!(ErrorCode::UnknownError => "An error occurred: {}", details);
168/// // or
169/// bail!("A guest error occurred: {}", details); // defaults to ErrorCode::GuestError
170/// ```
171#[macro_export]
172macro_rules! bail {
173    ($ec:expr => $($msg:tt)*) => {
174        return ::core::result::Result::Err($crate::error::HyperlightGuestError::new($ec, ::alloc::format!($($msg)*)));
175    };
176    ($($msg:tt)*) => {
177        $crate::bail!($crate::error::ErrorCode::GuestError => $($msg)*);
178    };
179}
180
181/// Macro to ensure a condition is true, otherwise returns early with a `Err(HyperlightGuestError)`.
182/// Usage:
183/// ```ignore
184/// ensure!(1 + 1 == 3, ErrorCode::UnknownError => "Maths is broken: {}", details);
185/// // or
186/// ensure!(1 + 1 == 3, "Maths is broken: {}", details); // defaults to ErrorCode::GuestError
187/// // or
188/// ensure!(1 + 1 == 3); // defaults to ErrorCode::GuestError with a default message
189/// ```
190#[macro_export]
191macro_rules! ensure {
192    ($cond:expr) => {
193        if !($cond) {
194            $crate::bail!(::core::concat!("Condition failed: `", ::core::stringify!($cond), "`"));
195        }
196    };
197    ($cond:expr, $ec:expr => $($msg:tt)*) => {
198        if !($cond) {
199            $crate::bail!($ec => ::core::concat!("{}\nCaused by failed condition: `", ::core::stringify!($cond), "`"), ::core::format_args!($($msg)*));
200        }
201    };
202    ($cond:expr, $($msg:tt)*) => {
203        $crate::ensure!($cond, $crate::error::ErrorCode::GuestError => $($msg)*);
204    };
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_context_option_some() {
213        let value: Option<u32> = Some(42);
214        let result = value.context("Should be Some");
215        assert_eq!(result.unwrap(), 42);
216    }
217
218    #[test]
219    fn test_context_option_none() {
220        let value: Option<u32> = None;
221        let result = value.context("Should be Some");
222        let err = result.unwrap_err();
223        assert_eq!(err.kind, ErrorCode::GuestError);
224        assert_eq!(err.message, "Should be Some");
225    }
226
227    #[test]
228    fn test_context_and_code_option_none() {
229        let value: Option<u32> = None;
230        let result = value.context_and_code(ErrorCode::MallocFailed, "Should be Some");
231        let err = result.unwrap_err();
232        assert_eq!(err.kind, ErrorCode::MallocFailed);
233        assert_eq!(err.message, "Should be Some");
234    }
235
236    #[test]
237    fn test_with_context_option_none() {
238        let value: Option<u32> = None;
239        let result = value.with_context(|| "Lazy context message");
240        let err = result.unwrap_err();
241        assert_eq!(err.kind, ErrorCode::GuestError);
242        assert_eq!(err.message, "Lazy context message");
243    }
244
245    #[test]
246    fn test_with_context_and_code_option_none() {
247        let value: Option<u32> = None;
248        let result =
249            value.with_context_and_code(ErrorCode::MallocFailed, || "Lazy context message");
250        let err = result.unwrap_err();
251        assert_eq!(err.kind, ErrorCode::MallocFailed);
252        assert_eq!(err.message, "Lazy context message");
253    }
254
255    #[test]
256    fn test_context_result_ok() {
257        let value: core::result::Result<u32, &str> = Ok(42);
258        let result = value.context("Should be Ok");
259        assert_eq!(result.unwrap(), 42);
260    }
261
262    #[test]
263    fn test_context_result_err() {
264        let value: core::result::Result<u32, &str> = Err("Some error");
265        let result = value.context("Should be Ok");
266        let err = result.unwrap_err();
267        assert_eq!(err.kind, ErrorCode::GuestError);
268        assert_eq!(err.message, "Should be Ok.\nCaused by: \"Some error\"");
269    }
270
271    #[test]
272    fn test_context_and_code_result_err() {
273        let value: core::result::Result<u32, &str> = Err("Some error");
274        let result = value.context_and_code(ErrorCode::MallocFailed, "Should be Ok");
275        let err = result.unwrap_err();
276        assert_eq!(err.kind, ErrorCode::MallocFailed);
277        assert_eq!(err.message, "Should be Ok.\nCaused by: \"Some error\"");
278    }
279
280    #[test]
281    fn test_with_context_result_err() {
282        let value: core::result::Result<u32, &str> = Err("Some error");
283        let result = value.with_context(|| "Lazy context message");
284        let err = result.unwrap_err();
285        assert_eq!(err.kind, ErrorCode::GuestError);
286        assert_eq!(
287            err.message,
288            "Lazy context message.\nCaused by: \"Some error\""
289        );
290    }
291
292    #[test]
293    fn test_with_context_and_code_result_err() {
294        let value: core::result::Result<u32, &str> = Err("Some error");
295        let result =
296            value.with_context_and_code(ErrorCode::MallocFailed, || "Lazy context message");
297        let err = result.unwrap_err();
298        assert_eq!(err.kind, ErrorCode::MallocFailed);
299        assert_eq!(
300            err.message,
301            "Lazy context message.\nCaused by: \"Some error\""
302        );
303    }
304
305    #[test]
306    fn test_bail_macro() {
307        let result: Result<u32> = (|| {
308            bail!("A guest error occurred");
309        })();
310        let err = result.unwrap_err();
311        assert_eq!(err.kind, ErrorCode::GuestError);
312        assert_eq!(err.message, "A guest error occurred");
313    }
314
315    #[test]
316    fn test_bail_macro_with_error_code() {
317        let result: Result<u32> = (|| {
318            bail!(ErrorCode::MallocFailed => "Memory allocation failed");
319        })();
320        let err = result.unwrap_err();
321        assert_eq!(err.kind, ErrorCode::MallocFailed);
322        assert_eq!(err.message, "Memory allocation failed");
323    }
324
325    #[test]
326    fn test_ensure_macro_pass() {
327        let result: Result<u32> = (|| {
328            ensure!(1 + 1 == 2, "Math works");
329            Ok(42)
330        })();
331        assert_eq!(result.unwrap(), 42);
332    }
333
334    #[test]
335    fn test_ensure_macro_fail() {
336        let result: Result<u32> = (|| {
337            ensure!(1 + 1 == 3, "Math is broken");
338            Ok(42)
339        })();
340        let err = result.unwrap_err();
341        assert_eq!(err.kind, ErrorCode::GuestError);
342        assert_eq!(
343            err.message,
344            "Math is broken\nCaused by failed condition: `1 + 1 == 3`"
345        );
346    }
347
348    #[test]
349    fn test_ensure_macro_fail_no_message() {
350        let result: Result<u32> = (|| {
351            ensure!(1 + 1 == 3);
352            Ok(42)
353        })();
354        let err = result.unwrap_err();
355        assert_eq!(err.kind, ErrorCode::GuestError);
356        assert_eq!(err.message, "Condition failed: `1 + 1 == 3`");
357    }
358
359    #[test]
360    fn test_ensure_macro_fail_with_error_code() {
361        let result: Result<u32> = (|| {
362            ensure!(1 + 1 == 3, ErrorCode::UnknownError => "Math is broken");
363            Ok(42)
364        })();
365        let err = result.unwrap_err();
366        assert_eq!(err.kind, ErrorCode::UnknownError);
367        assert_eq!(
368            err.message,
369            "Math is broken\nCaused by failed condition: `1 + 1 == 3`"
370        );
371    }
372}