Skip to content

Commit 828a529

Browse files
Make the component macros support passing host resources to guests (#839)
1 parent a835ef5 commit 828a529

File tree

12 files changed

+226
-60
lines changed

12 files changed

+226
-60
lines changed

Justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ like-ci config=default-target hypervisor="kvm":
140140
{{ if os() == "linux" { "just test-rust-tracing " + config + " " + if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } } else { "" } }}
141141

142142
@# Run benchmarks
143-
{{ if config == "release" { "just bench-ci main " + config + " " + if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } } else { "" } }}
143+
{{ if config == "release" { "just bench-ci main " + if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } } else { "" } }}
144144

145145
# runs all tests
146146
test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration "rust" target features) (test-integration "c" target features) (test-seccomp target features) (test-doc target features)

flake.nix

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
channel = "stable";
6060
sha256 = "sha256-AJ6LX/Q/Er9kS15bn9iflkUwcgYqRQxiOIL2ToVAXaU=";
6161
};
62+
"1.86" = {
63+
date = "2025-04-03";
64+
channel = "stable";
65+
sha256 = "sha256-X/4ZBHO3iW0fOenQ3foEvscgAPJYl2abspaBThDOukI=";
66+
};
6267
};
6368

6469
rust-platform = makeRustPlatform {

src/hyperlight_component_util/src/emit.rs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,28 @@ impl Mod {
196196
}
197197
}
198198

199+
/// Unlike [`tv::ResolvedTyvar`], which is mostly concerned with free
200+
/// variables and leaves bound variables alone, this tells us the most
201+
/// information that we have at codegen time for a top level bound
202+
/// variable.
203+
pub enum ResolvedBoundVar<'a> {
204+
Definite {
205+
/// The final variable offset (relative to s.var_offset) that
206+
/// we followed to get to this definite type, used
207+
/// occasionally to name things.
208+
final_bound_var: u32,
209+
/// The actual definite type that this resolved to
210+
ty: Defined<'a>,
211+
},
212+
Resource {
213+
/// A resource-type index. Currently a resource-type index is
214+
/// the same as the de Bruijn index of the tyvar that
215+
/// introduced the resource type, but is never affected by
216+
/// e.g. s.var_offset.
217+
rtidx: u32,
218+
},
219+
}
220+
199221
/// A whole grab-bag of useful state to have while emitting Rust
200222
#[derive(Debug)]
201223
pub struct State<'a, 'b> {
@@ -260,6 +282,8 @@ pub struct State<'a, 'b> {
260282
/// wasmtime guest emit. When that is refactored to use the host
261283
/// guest emit, this can go away.
262284
pub is_wasmtime_guest: bool,
285+
/// Are we working on an export or an import of the component type?
286+
pub is_export: bool,
263287
}
264288

265289
/// Create a State with all of its &mut references pointing to
@@ -311,6 +335,7 @@ impl<'a, 'b> State<'a, 'b> {
311335
root_component_name: None,
312336
is_guest,
313337
is_wasmtime_guest,
338+
is_export: false,
314339
}
315340
}
316341
pub fn clone<'c>(&'c mut self) -> State<'c, 'b> {
@@ -331,6 +356,7 @@ impl<'a, 'b> State<'a, 'b> {
331356
root_component_name: self.root_component_name.clone(),
332357
is_guest: self.is_guest,
333358
is_wasmtime_guest: self.is_wasmtime_guest,
359+
is_export: self.is_export,
334360
}
335361
}
336362
/// Obtain a reference to the [`Mod`] that we are currently
@@ -508,9 +534,17 @@ impl<'a, 'b> State<'a, 'b> {
508534
}
509535
/// Add an import/export to [`State::origin`], reflecting that we are now
510536
/// looking at code underneath it
511-
pub fn push_origin<'c>(&'c mut self, is_export: bool, name: &'b str) -> State<'c, 'b> {
537+
///
538+
/// origin_was_export differs from s.is_export in that s.is_export
539+
/// keeps track of whether the item overall was imported or exported
540+
/// from the root component (taking into account positivity), whereas
541+
/// origin_was_export just checks if this particular extern_decl was
542+
/// imported or exported from its parent instance (and so e.g. an
543+
/// export of an instance that is imported by the root component has
544+
/// !s.is_export && origin_was_export)
545+
pub fn push_origin<'c>(&'c mut self, origin_was_export: bool, name: &'b str) -> State<'c, 'b> {
512546
let mut s = self.clone();
513-
s.origin.push(if is_export {
547+
s.origin.push(if origin_was_export {
514548
ImportExport::Export(name)
515549
} else {
516550
ImportExport::Import(name)
@@ -588,15 +622,24 @@ impl<'a, 'b> State<'a, 'b> {
588622
/// up with a definition, in which case, let's get that, or it
589623
/// ends up with a resource type, in which case we return the
590624
/// resource index
591-
pub fn resolve_tv(&self, n: u32) -> (u32, Option<Defined<'b>>) {
592-
match &self.bound_vars[self.var_offset + n as usize].bound {
625+
///
626+
/// Distinct from [`Ctx::resolve_tv`], which is mostly concerned
627+
/// with free variables, because this is concerned entirely with
628+
/// bound variables.
629+
pub fn resolve_bound_var(&self, n: u32) -> ResolvedBoundVar<'b> {
630+
let noff = self.var_offset as u32 + n;
631+
match &self.bound_vars[noff as usize].bound {
593632
TypeBound::Eq(Defined::Handleable(Handleable::Var(Tyvar::Bound(nn)))) => {
594-
self.resolve_tv(n + 1 + nn)
633+
self.resolve_bound_var(n + 1 + nn)
595634
}
596-
TypeBound::Eq(t) => (n, Some(t.clone())),
597-
TypeBound::SubResource => (n, None),
635+
TypeBound::Eq(t) => ResolvedBoundVar::Definite {
636+
final_bound_var: n,
637+
ty: t.clone(),
638+
},
639+
TypeBound::SubResource => ResolvedBoundVar::Resource { rtidx: noff },
598640
}
599641
}
642+
600643
/// Construct a namespace path referring to the resource trait for
601644
/// a resource with the given name
602645
pub fn resource_trait_path(&self, r: Ident) -> Vec<Ident> {

src/hyperlight_component_util/src/guest.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ use proc_macro2::TokenStream;
1818
use quote::{format_ident, quote};
1919

2020
use crate::emit::{
21-
FnName, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn, kebab_to_getter,
22-
kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, split_wit_name,
21+
FnName, ResolvedBoundVar, ResourceItemName, State, WitName, kebab_to_exports_name, kebab_to_fn,
22+
kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var,
23+
split_wit_name,
2324
};
2425
use crate::etypes::{Component, Defined, ExternDecl, ExternDesc, Handleable, Instance, Tyvar};
2526
use crate::hl::{
@@ -98,10 +99,10 @@ fn emit_import_extern_decl<'a, 'b, 'c>(
9899
ExternDesc::Type(t) => match t {
99100
Defined::Handleable(Handleable::Var(Tyvar::Bound(b))) => {
100101
// only resources need something emitted
101-
let (b, None) = s.resolve_tv(*b) else {
102+
let ResolvedBoundVar::Resource { rtidx } = s.resolve_bound_var(*b) else {
102103
return quote! {};
103104
};
104-
let rtid = format_ident!("HostResource{}", s.var_offset + b as usize);
105+
let rtid = format_ident!("HostResource{}", rtidx as usize);
105106
let path = s.resource_trait_path(kebab_to_type(ed.kebab_name));
106107
s.root_mod
107108
.r#impl(path, format_ident!("Host"))
@@ -314,6 +315,8 @@ fn emit_component<'a, 'b, 'c>(
314315

315316
s.var_offset = 0;
316317

318+
s.is_export = true;
319+
317320
let exports = ct
318321
.instance
319322
.unqualified

src/hyperlight_component_util/src/hl.rs

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ use itertools::Itertools;
1818
use proc_macro2::{Ident, TokenStream};
1919
use quote::{format_ident, quote};
2020

21-
use crate::emit::{State, kebab_to_cons, kebab_to_var};
22-
use crate::etypes::{self, Defined, Handleable, TypeBound, Tyvar, Value};
21+
use crate::emit::{ResolvedBoundVar, State, kebab_to_cons, kebab_to_var};
22+
use crate::etypes::{self, Defined, Handleable, Tyvar, Value};
2323
use crate::rtypes;
2424

2525
/// Construct a string that can be used "on the wire" to identify a
@@ -151,25 +151,16 @@ pub fn emit_hl_unmarshal_toplevel_value(
151151
}
152152
}
153153

154-
/// Find the resource index that the given type variable refers to.
155-
///
156-
/// Precondition: this type variable does refer to a resource type
157-
fn resolve_tyvar_to_resource(s: &mut State, v: u32) -> u32 {
158-
match s.bound_vars[v as usize].bound {
159-
TypeBound::SubResource => v,
160-
TypeBound::Eq(Defined::Handleable(Handleable::Var(Tyvar::Bound(vv)))) => {
161-
resolve_tyvar_to_resource(s, v + vv + 1)
162-
}
163-
_ => panic!("impossible: resource var is not resource"),
164-
}
165-
}
166154
/// Find the resource index that the given Handleable refers to.
167155
///
168156
/// Precondition: this type variable does refer to a resource type
169157
pub fn resolve_handleable_to_resource(s: &mut State, ht: &Handleable) -> u32 {
170158
match ht {
171159
Handleable::Var(Tyvar::Bound(vi)) => {
172-
resolve_tyvar_to_resource(s, s.var_offset as u32 + *vi)
160+
let ResolvedBoundVar::Resource { rtidx } = s.resolve_bound_var(*vi) else {
161+
panic!("impossible: resource var is not resource");
162+
};
163+
rtidx
173164
}
174165
_ => panic!("impossible handleable in type"),
175166
}
@@ -338,9 +329,29 @@ pub fn emit_hl_unmarshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStr
338329
log::debug!("resolved ht to r (2) {:?} {:?}", ht, vi);
339330
if s.is_guest {
340331
let rid = format_ident!("HostResource{}", vi);
341-
quote! {
342-
let i = u32::from_ne_bytes(#id[0..4].try_into().unwrap());
343-
(::wasmtime::component::Resource::<#rid>::new_borrow(i), 4)
332+
if s.is_wasmtime_guest {
333+
quote! {
334+
let i = u32::from_ne_bytes(#id[0..4].try_into().unwrap());
335+
(::wasmtime::component::Resource::<#rid>::new_borrow(i), 4)
336+
}
337+
} else {
338+
// TODO: When we add the Drop impl (#810), we need
339+
// to make sure it does not get called here
340+
//
341+
// If we tried to actually return a reference
342+
// here, rustc would get mad about the temporary
343+
// constructed here not living long enough, so
344+
// instead we return the temporary and construct
345+
// the reference elsewhere. It might be a bit more
346+
// principled to have a separate
347+
// HostResourceXXBorrow struct that implements
348+
// AsRef<HostResourceXX> or something in the
349+
// future...
350+
quote! {
351+
let i = u32::from_ne_bytes(#id[0..4].try_into().unwrap());
352+
353+
(#rid { rep: i }, 4)
354+
}
344355
}
345356
} else {
346357
let rid = format_ident!("resource{}", vi);
@@ -358,7 +369,11 @@ pub fn emit_hl_unmarshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStr
358369
let Some(Tyvar::Bound(n)) = tv else {
359370
panic!("impossible tyvar")
360371
};
361-
let (n, Some(Defined::Value(vt))) = s.resolve_tv(*n) else {
372+
let ResolvedBoundVar::Definite {
373+
final_bound_var: n,
374+
ty: Defined::Value(vt),
375+
} = s.resolve_bound_var(*n)
376+
else {
362377
panic!("unresolvable tyvar (2)");
363378
};
364379
let vt = vt.clone();
@@ -644,7 +659,9 @@ pub fn emit_hl_marshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStrea
644659
let rid = format_ident!("resource{}", vi);
645660
quote! {
646661
let i = rts.#rid.len();
647-
rts.#rid.push_back(::hyperlight_common::resource::ResourceEntry::lend(#id));
662+
let (lrg, re) = ::hyperlight_common::resource::ResourceEntry::lend(#id);
663+
to_cleanup.push(Box::new(lrg));
664+
rts.#rid.push_back(re);
648665
alloc::vec::Vec::from(u32::to_ne_bytes(i as u32))
649666
}
650667
}
@@ -653,7 +670,11 @@ pub fn emit_hl_marshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStrea
653670
let Some(Tyvar::Bound(n)) = tv else {
654671
panic!("impossible tyvar")
655672
};
656-
let (n, Some(Defined::Value(vt))) = s.resolve_tv(*n) else {
673+
let ResolvedBoundVar::Definite {
674+
final_bound_var: n,
675+
ty: Defined::Value(vt),
676+
} = s.resolve_bound_var(*n)
677+
else {
657678
panic!("unresolvable tyvar (2)");
658679
};
659680
let vt = vt.clone();
@@ -668,7 +689,20 @@ pub fn emit_hl_marshal_value(s: &mut State, id: Ident, vt: &Value) -> TokenStrea
668689
/// [`crate::rtypes`] module) of the given value type.
669690
pub fn emit_hl_unmarshal_param(s: &mut State, id: Ident, pt: &Value) -> TokenStream {
670691
let toks = emit_hl_unmarshal_value(s, id, pt);
671-
quote! { { #toks }.0 }
692+
// Slight hack to avoid rust complaints about deserialised
693+
// resource borrow lifetimes.
694+
fn is_borrow(vt: &Value) -> bool {
695+
match vt {
696+
Value::Borrow(_) => true,
697+
Value::Var(_, vt) => is_borrow(vt),
698+
_ => false,
699+
}
700+
}
701+
if s.is_guest && !s.is_wasmtime_guest && is_borrow(pt) {
702+
quote! { &({ #toks }.0) }
703+
} else {
704+
quote! { { #toks }.0 }
705+
}
672706
}
673707

674708
/// Emit code to unmarshal the result of a function with result type

src/hyperlight_component_util/src/host.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,20 @@ fn emit_export_extern_decl<'a, 'b, 'c>(
5656
let unmarshal = emit_hl_unmarshal_result(s, ret.clone(), &ft.result);
5757
quote! {
5858
fn #n(&mut self, #(#param_decls),*) -> #result_decl {
59+
let mut to_cleanup = Vec::<Box<dyn Drop>>::new();
60+
let marshalled = {
61+
let mut rts = self.rt.lock().unwrap();
62+
#[allow(clippy::unused_unit)]
63+
(#(#marshal,)*)
64+
};
5965
let #ret = ::hyperlight_host::sandbox::Callable::call::<::std::vec::Vec::<u8>>(&mut self.sb,
6066
#hln,
61-
(#(#marshal,)*)
67+
marshalled,
6268
);
6369
let ::std::result::Result::Ok(#ret) = #ret else { panic!("bad return from guest {:?}", #ret) };
6470
#[allow(clippy::unused_unit)]
71+
let mut rts = self.rt.lock().unwrap();
72+
#[allow(clippy::unused_unit)]
6573
#unmarshal
6674
}
6775
}
@@ -333,6 +341,8 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com
333341
s.root_component_name = Some((ns.clone(), wn.name));
334342
s.cur_trait = Some(export_trait.clone());
335343
s.import_param_var = Some(format_ident!("I"));
344+
s.is_export = true;
345+
336346
let exports = ct
337347
.instance
338348
.unqualified

src/hyperlight_component_util/src/rtypes.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,11 @@ pub fn emit_value(s: &mut State, vt: &Value) -> TokenStream {
345345
}
346346
} else {
347347
let vr = emit_var_ref(s, tv);
348-
quote! { ::hyperlight_common::resource::BorrowedResourceGuard<#vr> }
348+
if s.is_export {
349+
quote! { &#vr }
350+
} else {
351+
quote! { ::hyperlight_common::resource::BorrowedResourceGuard<#vr> }
352+
}
349353
}
350354
}
351355
},
@@ -607,16 +611,19 @@ fn emit_type_alias<F: Fn(&mut State) -> TokenStream>(
607611

608612
/// Emit (via returning) a Rust trait item corresponding to this
609613
/// extern decl
614+
///
615+
/// See note on emit.rs push_origin for the difference between
616+
/// origin_was_export and s.is_export.
610617
fn emit_extern_decl<'a, 'b, 'c>(
611-
is_export: bool,
618+
origin_was_export: bool,
612619
s: &'c mut State<'a, 'b>,
613620
ed: &'c ExternDecl<'b>,
614621
) -> TokenStream {
615622
log::debug!(" emitting decl {:?}", ed.kebab_name);
616623
match &ed.desc {
617624
ExternDesc::CoreModule(_) => panic!("core module (im/ex)ports are not supported"),
618625
ExternDesc::Func(ft) => {
619-
let mut s = s.push_origin(is_export, ed.kebab_name);
626+
let mut s = s.push_origin(origin_was_export, ed.kebab_name);
620627
match kebab_to_fn(ed.kebab_name) {
621628
FnName::Plain(n) => {
622629
let params = ft
@@ -681,7 +688,7 @@ fn emit_extern_decl<'a, 'b, 'c>(
681688
TokenStream::new()
682689
}
683690
let edn: &'b str = ed.kebab_name;
684-
let mut s: State<'_, 'b> = s.push_origin(is_export, edn);
691+
let mut s: State<'_, 'b> = s.push_origin(origin_was_export, edn);
685692
if let Some((n, bound)) = s.is_var_defn(t) {
686693
match bound {
687694
TypeBound::Eq(t) => {
@@ -708,7 +715,7 @@ fn emit_extern_decl<'a, 'b, 'c>(
708715
}
709716
}
710717
ExternDesc::Instance(it) => {
711-
let mut s = s.push_origin(is_export, ed.kebab_name);
718+
let mut s = s.push_origin(origin_was_export, ed.kebab_name);
712719
let wn = split_wit_name(ed.kebab_name);
713720
emit_instance(&mut s, wn.clone(), it);
714721

@@ -831,8 +838,8 @@ fn emit_component<'a, 'b, 'c>(s: &'c mut State<'a, 'b>, wn: WitName, ct: &'c Com
831838
s.cur_trait().items.extend(quote! { #(#imports)* });
832839

833840
s.adjust_vars(ct.instance.evars.len() as u32);
834-
835841
s.import_param_var = Some(format_ident!("I"));
842+
s.is_export = true;
836843

837844
let export_name = kebab_to_exports_name(wn.name);
838845
*s.bound_vars = ct

0 commit comments

Comments
 (0)