Skip to content

Commit 63e5d7d

Browse files
committed
[hyperlight_component_util] Support passing host resources to guest functions
Previously, the guest could acquire and use host resources through host function calls, but the original call from the host into the guest could not pass in owned or borrowed host resources. This commit fixes in a number of bugs in order to make that possible, and adds rudimentary tests to witguest.
1 parent 6b02c88 commit 63e5d7d

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-
just bench-ci main {{config}} {{ if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } }}
143+
just bench-ci main {{ if hypervisor == "mshv" { "mshv2" } else if hypervisor == "mshv3" { "mshv3" } else { "kvm" } }}
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)