diff --git a/CHANGELOG.md b/CHANGELOG.md index 571a2ec98..643e3092f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed * Fix symbol resolution in guest core dumps for sandboxes created from snapshots by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1618 * Reject malformed OCI snapshot metadata and non-regular artifact files during load. +* Return guest call failures from `host_bindgen!` functions as `HyperlightError` values. ## [v0.16.0] - 2026-06-26 diff --git a/src/hyperlight_component_util/src/emit.rs b/src/hyperlight_component_util/src/emit.rs index 143eef91f..9a4ec6462 100644 --- a/src/hyperlight_component_util/src/emit.rs +++ b/src/hyperlight_component_util/src/emit.rs @@ -149,6 +149,10 @@ pub struct Trait { pub tvs: BTreeMap, TokenStream)>, /// Raw tokens of the contents of the trait pub items: TokenStream, + /// Trait items keyed by their WIT names. A resource can be reached + /// more than once, e.g. via both the import-side and the export-side + /// trait of an instance, and its trait is shared. + pub extern_decls: BTreeMap, } impl Trait { pub fn new() -> Self { @@ -156,6 +160,7 @@ impl Trait { supertraits: BTreeMap::new(), tvs: BTreeMap::new(), items: TokenStream::new(), + extern_decls: BTreeMap::new(), } } /// Collect the component tyvar indices that correspond to the @@ -217,8 +222,9 @@ impl Trait { .collect::>(); let tvs = self.tv_toks(); let items = &self.items; + let extern_decls = self.extern_decls.values(); quote! { - pub trait #n #tvs #trait_colon #(#supertraits)+* { #items } + pub trait #n #tvs #trait_colon #(#supertraits)+* { #items #(#extern_decls)* } } } } @@ -230,6 +236,10 @@ impl Trait { pub struct Mod { pub submods: BTreeMap, pub items: TokenStream, + /// Definitions keyed by their WIT names. An instance can be reached + /// more than once, e.g. as both an import and an export of a component, + /// and its helper module is shared. + pub extern_decls: BTreeMap, pub traits: BTreeMap, pub impls: BTreeMap<(Vec, Ident), TokenStream>, } @@ -238,6 +248,7 @@ impl Mod { Self { submods: BTreeMap::new(), items: TokenStream::new(), + extern_decls: BTreeMap::new(), traits: BTreeMap::new(), impls: BTreeMap::new(), } @@ -294,6 +305,9 @@ impl Mod { for (n, mut t) in self.traits { tt.extend(t.into_tokens(n)); } + for (_, decl) in self.extern_decls { + tt.extend(decl); + } tt.extend(self.items); for ((ns, i), t) in self.impls { tt.extend(quote! { diff --git a/src/hyperlight_component_util/src/host.rs b/src/hyperlight_component_util/src/host.rs index eb94c699e..c80f269aa 100644 --- a/src/hyperlight_component_util/src/host.rs +++ b/src/hyperlight_component_util/src/host.rs @@ -59,19 +59,18 @@ fn emit_export_extern_decl<'a, 'b, 'c>( fn #n(&mut self, #(#param_decls),*) -> #result_decl { let mut to_cleanup = Vec::>::new(); let marshalled = { - let mut rts = self.rt.lock().unwrap(); + let mut rts = self.rt.lock()?; #[allow(clippy::unused_unit)] (#(#marshal,)*) }; let #ret = ::hyperlight_host::sandbox::Callable::call::<::std::vec::Vec::>(&mut self.sb, #hln, marshalled, - ); - let ::std::result::Result::Ok(#ret) = #ret else { panic!("bad return from guest {:?}", #ret) }; + )?; #[allow(clippy::unused_unit)] - let mut rts = self.rt.lock().unwrap(); + let mut rts = self.rt.lock()?; #[allow(clippy::unused_unit)] - #unmarshal + Ok(#unmarshal) } } } @@ -122,7 +121,7 @@ fn emit_export_instance<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, it: & let ns = wn.namespace_path(); let nsi = wn.namespace_idents(); - let trait_name = kebab_to_type(wn.name); + let trait_name = kebab_to_exports_name(wn.name); let r#trait = s.r#trait(&nsi, trait_name.clone()); let tvs = r#trait .tvs @@ -166,7 +165,7 @@ impl SelfInfo { orig_id, type_id: vec![format_ident!("I")], inner_preamble: quote! { - let mut #inner_id = #outer_id.lock().unwrap(); + let mut #inner_id = #outer_id.lock()?; let mut #inner_id = ::std::ops::DerefMut::deref_mut(&mut #inner_id); }, outer_id, @@ -245,7 +244,7 @@ fn emit_import_extern_decl<'a, 'b, 'c>( let #outer_id = #orig_id.clone(); let captured_rts = rts.clone(); sb.register_host_function(#hln, move |#(#pds),*| { - let mut rts = captured_rts.lock().unwrap(); + let mut rts = captured_rts.lock()?; #inner_preamble let #ret = #callname( ::std::borrow::BorrowMut::<#(#type_id)::*>::borrow_mut( diff --git a/src/hyperlight_component_util/src/resource.rs b/src/hyperlight_component_util/src/resource.rs index a84e46bd8..aa6833b8a 100644 --- a/src/hyperlight_component_util/src/resource.rs +++ b/src/hyperlight_component_util/src/resource.rs @@ -99,7 +99,7 @@ pub fn emit_tables<'a, 'b, 'c>( #sphantom } impl #rtsid { - fn new() -> Self { + pub(crate) fn new() -> Self { #rtsid { #(#inits,)* _phantomI: ::core::marker::PhantomData, diff --git a/src/hyperlight_component_util/src/rtypes.rs b/src/hyperlight_component_util/src/rtypes.rs index ffa5041c1..322a4e992 100644 --- a/src/hyperlight_component_util/src/rtypes.rs +++ b/src/hyperlight_component_util/src/rtypes.rs @@ -573,9 +573,14 @@ pub fn emit_func_param(s: &mut State, p: &Param) -> TokenStream { /// Precondition: the result type must only be a named result if there /// are no names in it (i.e. a unit type) pub fn emit_func_result(s: &mut State, r: &etypes::Result<'_>) -> TokenStream { - match r { + let result = match r { Some(vt) => emit_value(s, vt), None => quote! { () }, + }; + if !s.is_guest && s.is_export { + quote! { ::hyperlight_host::Result<#result> } + } else { + result } } @@ -681,6 +686,9 @@ fn emit_extern_decl<'a, 'b, 'c>( FnName::Associated(r, n) => { let mut s = s.helper(); s.cur_trait = Some(r.clone()); + if s.cur_trait().extern_decls.contains_key(ed.kebab_name) { + return quote! {}; + } let mut needs_vars = BTreeSet::new(); let mut sv = s.with_needs_vars(&mut needs_vars); let params = ft @@ -690,21 +698,24 @@ fn emit_extern_decl<'a, 'b, 'c>( .collect::>(); match n { ResourceItemName::Constructor => { - sv.cur_trait().items.extend(quote! { - fn new(&mut self, #(#params),*) -> Self::T; - }); + sv.cur_trait().extern_decls.insert( + ed.kebab_name.to_string(), + quote! { fn new(&mut self, #(#params),*) -> Self::T; }, + ); } ResourceItemName::Method(n) => { let result = emit_func_result(&mut sv, &ft.result); - sv.cur_trait().items.extend(quote! { - fn #n(&mut self, #(#params),*) -> #result; - }); + sv.cur_trait().extern_decls.insert( + ed.kebab_name.to_string(), + quote! { fn #n(&mut self, #(#params),*) -> #result; }, + ); } ResourceItemName::Static(n) => { let result = emit_func_result(&mut sv, &ft.result); - sv.cur_trait().items.extend(quote! { - fn #n(&mut self, #(#params),*) -> #result; - }); + sv.cur_trait().extern_decls.insert( + ed.kebab_name.to_string(), + quote! { fn #n(&mut self, #(#params),*) -> #result; }, + ); } } for v in needs_vars { @@ -724,9 +735,14 @@ fn emit_extern_decl<'a, 'b, 'c>( ) -> TokenStream { let id = kebab_to_type(ed.kebab_name); let mut s = s.helper(); + if s.cur_mod().extern_decls.contains_key(ed.kebab_name) { + return TokenStream::new(); + } let t = emit_defined(&mut s, v, id, t); - s.cur_mod().items.extend(t); + s.cur_mod() + .extern_decls + .insert(ed.kebab_name.to_string(), t); TokenStream::new() } let edn: &'b str = ed.kebab_name; @@ -746,9 +762,12 @@ fn emit_extern_decl<'a, 'b, 'c>( s.add_helper_supertrait(rn.clone()); let mut s = s.helper(); s.cur_trait = Some(rn.clone()); - s.cur_trait().items.extend(quote! { - type T: ::core::marker::Send; - }); + if !s.cur_trait().extern_decls.contains_key(ed.kebab_name) { + s.cur_trait().extern_decls.insert( + ed.kebab_name.to_string(), + quote! { type T: ::core::marker::Send; }, + ); + } quote! {} } } @@ -762,7 +781,12 @@ fn emit_extern_decl<'a, 'b, 'c>( emit_instance(&mut s, wn.clone(), it); let nsids = wn.namespace_idents(); - let repr = s.r#trait(&nsids, kebab_to_type(wn.name)); + let trait_tn = if !s.is_guest && s.is_export { + kebab_to_exports_name(wn.name) + } else { + kebab_to_type(wn.name) + }; + let repr = s.r#trait(&nsids, trait_tn.clone()); let vs = if !repr.tvs.is_empty() { let vs = repr.tvs.clone(); let tvs = vs @@ -780,7 +804,6 @@ fn emit_extern_decl<'a, 'b, 'c>( }; let rp = s.root_path(); let tns = wn.namespace_path(); - let trait_tn = kebab_to_type(wn.name); let trait_bound = if tns.is_empty() { quote! { #rp #trait_tn } } else { @@ -803,7 +826,11 @@ fn emit_instance<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, it: &'c Inst tracing::debug!("emitting instance {:?}", wn); let mut s = s.with_cursor(wn.namespace_idents()); - let name = kebab_to_type(wn.name); + let name = if !s.is_guest && s.is_export { + kebab_to_exports_name(wn.name) + } else { + kebab_to_type(wn.name) + }; s.cur_helper_mod = Some(kebab_to_namespace(wn.name)); s.cur_trait = Some(name.clone()); @@ -857,7 +884,6 @@ fn emit_instance<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, it: &'c Inst let id = s.noff_var_id(v); s.cur_trait().tvs.insert(id, (Some(v), TokenStream::new())); } - s.cur_trait().items.extend(quote! { #(#exports)* }); } diff --git a/src/hyperlight_host/tests/wit_test.rs b/src/hyperlight_host/tests/wit_test.rs index 1839c9236..f34f7e4ae 100644 --- a/src/hyperlight_host/tests/wit_test.rs +++ b/src/hyperlight_host/tests/wit_test.rs @@ -293,9 +293,12 @@ fn sb() -> TestSandbox { mod wit_test { + use hyperlight_host::HyperlightError; use proptest::prelude::*; - use crate::bindings::test::wit::{Roundtrip, TestExports, TestHostResource, roundtrip}; + use crate::bindings::test::wit::{ + FailableExports, RoundtripExports, TestExports, TestHostResourceExports, roundtrip, + }; use crate::sb; prop_compose! { @@ -347,7 +350,7 @@ mod wit_test { proptest! { #[test] fn $fn(x $($ty)*) { - assert_eq!(x, sb().roundtrip().$fn(x.clone())) + assert_eq!(x, sb().roundtrip().$fn(x.clone()).unwrap()) } } } @@ -394,7 +397,16 @@ mod wit_test { #[test] fn test_roundtrip_no_result() { - sb().roundtrip().roundtrip_no_result(42); + sb().roundtrip().roundtrip_no_result(42).unwrap(); + } + + #[test] + fn test_guest_trap_returns_error() { + let err = sb().failable().will_trap().unwrap_err(); + assert!( + matches!(err, HyperlightError::GuestAborted(_, _)), + "unexpected error: {err:?}" + ); } use std::sync::atomic::Ordering::Relaxed; @@ -404,7 +416,7 @@ mod wit_test { let guard = crate::SERIALIZE_TEST_RESOURCE_TESTS.lock(); crate::HAS_BEEN_DROPPED.store(false, Relaxed); { - sb().test_host_resource().test_uses_locally(); + sb().test_host_resource().test_uses_locally().unwrap(); } assert!(crate::HAS_BEEN_DROPPED.load(Relaxed)); drop(guard); @@ -416,10 +428,10 @@ mod wit_test { { let mut sb = sb(); let inst = sb.test_host_resource(); - let r = inst.test_makes(); - inst.test_accepts_borrow(&r); - inst.test_accepts_own(r); - inst.test_returns(); + let r = inst.test_makes().unwrap(); + inst.test_accepts_borrow(&r).unwrap(); + inst.test_accepts_own(r).unwrap(); + inst.test_returns().unwrap(); } assert!(crate::HAS_BEEN_DROPPED.load(Relaxed)); drop(guard); @@ -495,29 +507,35 @@ mod bindgen_test_cases { #[allow(dead_code)] struct ExportHost; - impl test::bindgen_test_cases::Executor for ExportHost { - fn execute(&mut self) -> test::bindgen_test_cases::executor::ExecutionResult { - test::bindgen_test_cases::executor::ExecutionResult { + impl test::bindgen_test_cases::ExecutorExports for ExportHost { + fn execute( + &mut self, + ) -> hyperlight_host::Result { + Ok(test::bindgen_test_cases::executor::ExecutionResult { message: String::from("executed"), - } + }) } } - impl test::bindgen_test_cases::Types for ExportHost { - fn get_status(&mut self) -> test::bindgen_test_cases::types::Status { - test::bindgen_test_cases::types::Status { + impl test::bindgen_test_cases::TypesExports for ExportHost { + fn get_status( + &mut self, + ) -> hyperlight_host::Result { + Ok(test::bindgen_test_cases::types::Status { message: String::from("ok"), - } + }) } } - impl test::bindgen_test_cases::UsesExportedTypes + impl test::bindgen_test_cases::UsesExportedTypesExports for ExportHost { - fn get_status(&mut self) -> test::bindgen_test_cases::types::Status { - test::bindgen_test_cases::types::Status { + fn get_status( + &mut self, + ) -> hyperlight_host::Result { + Ok(test::bindgen_test_cases::types::Status { message: String::from("ok"), - } + }) } } @@ -571,3 +589,25 @@ mod inline_bindgen_test { assert_eq!(result.value, "inline"); } } + +/// The import and export traits share one helper module. Its type definitions +/// must be emitted once to avoid duplicate definitions during macro expansion. +mod shared_interface_bindings { + hyperlight_component_macro::host_bindgen!({ + inline: r#" + package test:shared-iface; + + world shared-world { + import shared; + export shared; + } + + interface shared { + record item { + value: u32, + } + } + "#, + world: "shared-world", + }); +} diff --git a/src/tests/rust_guests/witguest/guest.wit b/src/tests/rust_guests/witguest/guest.wit index 9ed164b80..3000e0105 100644 --- a/src/tests/rust_guests/witguest/guest.wit +++ b/src/tests/rust_guests/witguest/guest.wit @@ -5,6 +5,7 @@ world test { import host-resource; export roundtrip; export test-host-resource; + export failable; } interface roundtrip { @@ -89,4 +90,8 @@ interface test-host-resource { test-accepts-borrow: func(x: borrow); test-accepts-own: func(x: own); test-returns: func() -> own; +} + +interface failable { + will-trap: func() -> string; } \ No newline at end of file diff --git a/src/tests/rust_guests/witguest/src/main.rs b/src/tests/rust_guests/witguest/src/main.rs index 307d9cca6..57219a97d 100644 --- a/src/tests/rust_guests/witguest/src/main.rs +++ b/src/tests/rust_guests/witguest/src/main.rs @@ -204,6 +204,12 @@ impl test::wit::TestHostResource<::T> for Guest { } } +impl test::wit::Failable for Guest { + fn will_trap(&mut self) -> String { + panic!("deliberate guest crash") + } +} + #[allow(refining_impl_trait)] impl test::wit::TestExports for Guest { type Roundtrip = Self; @@ -214,6 +220,10 @@ impl test::wit::TestExports for Guest { fn test_host_resource(&mut self) -> &mut Self { self } + type Failable = Self; + fn failable(&mut self) -> &mut Self { + self + } } static GUEST_STATE: Mutex = Mutex::new(Guest {