From b5e04a08de23b47b63675e4cfefa119ced85c5ac Mon Sep 17 00:00:00 2001 From: Shailesh Vashishth Date: Sat, 8 Nov 2025 12:27:42 +0530 Subject: [PATCH] Closes #651: - adds support to select a specific world from a WIT file if multiple worlds are specified. Signed-off-by: Shailesh Vashishth --- src/hyperlight_component_macro/src/lib.rs | 88 ++++ .../src/component.rs | 91 ++++ src/hyperlight_component_util/src/util.rs | 33 ++ .../tests/host_bingen2_test.rs | 428 ++++++++++++++++++ 4 files changed, 640 insertions(+) create mode 100644 src/hyperlight_host/tests/host_bingen2_test.rs diff --git a/src/hyperlight_component_macro/src/lib.rs b/src/hyperlight_component_macro/src/lib.rs index 2d7fd9992..3664fef07 100644 --- a/src/hyperlight_component_macro/src/lib.rs +++ b/src/hyperlight_component_macro/src/lib.rs @@ -50,6 +50,8 @@ limitations under the License. extern crate proc_macro; use hyperlight_component_util::*; +use syn::parse::{Parse, ParseStream}; +use syn::{Ident, LitStr, Result, Token}; /// Create host bindings for the wasm component type in the file /// passed in (or `$WIT_WORLD`, if nothing is passed in). This will @@ -63,6 +65,7 @@ use hyperlight_component_util::*; /// `instantiate()` method on the component trait that makes /// instantiating the sandbox particularly ergonomic in core /// Hyperlight. + #[proc_macro] pub fn host_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let _ = env_logger::try_init(); @@ -79,6 +82,48 @@ pub fn host_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } +#[proc_macro] +pub fn host_bindgen2(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let _ = env_logger::try_init(); + + let parsed_bindgen_input = syn::parse_macro_input!(input as BindgenInputParams); + + eprintln!("WE GET BACK FROM THE PARSING"); + eprintln!("{:?}", parsed_bindgen_input); + + let path = parsed_bindgen_input.path.unwrap_or_else(|| { + let wit_world_env = std::env::var_os("WIT_WORLD"); + + if let Some(env) = wit_world_env { + std::path::PathBuf::from(env) + } else { + std::path::PathBuf::new() + } + }); + + let world_name = parsed_bindgen_input.world_name; + + // what do we do, do we disturb the function signature or + // put this world_name as an env or a rust static variable. + // keeping as a rust static variable seems to be an appropriate + // choice. frequently changing the OS env will be stupid + + eprintln!("PATH = {:?} \n WORLD_NAME = {:?}", path.clone().into_os_string(), world_name.clone()); + + + util::read_world_from_file_1( + path.into_os_string(), + world_name, + |kebab_name, ct: &etypes::Component<'_>| { + let decls = emit::run_state(false, false, |s| { + rtypes::emit_toplevel(s, &kebab_name, ct); + host::emit_toplevel(s, &kebab_name, ct); + }); + util::emit_decls(decls).into() + }, + ) +} + /// Create the hyperlight_guest_init() function (which should be /// called in hyperlight_main()) for the wasm component type in the /// file passed in (or `$WIT_WORLD`, if nothing is passed in). This @@ -107,3 +152,46 @@ pub fn guest_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream util::emit_decls(decls).into() }) } + +#[derive(Debug)] +struct BindgenInputParams { + world_name: Option, + path: Option, +} + +impl Parse for BindgenInputParams { + fn parse(input: ParseStream) -> Result { + let content; + syn::braced!(content in input); + eprintln!("Content = \n {:?}", content); + + let mut world_name = None; + let mut path = None; + + // Parse key-value pairs inside the braces + while !content.is_empty() { + let key: Ident = content.parse()?; + content.parse::()?; + + match key.to_string().as_str() { + "world_name" => { + let value: LitStr = content.parse()?; + world_name = Some(value.value()); + } + "path" => { + let value: LitStr = content.parse()?; + path = Some(std::path::PathBuf::from(value.value())); + } + _ => { + return Err(syn::Error::new(key.span(), format!("Unknown key: {}", key))); + } + } + + // Parse optional comma + if content.peek(Token![,]) { + content.parse::()?; + } + } + Ok(Self { world_name, path }) + } +} diff --git a/src/hyperlight_component_util/src/component.rs b/src/hyperlight_component_util/src/component.rs index 866419abc..083fb860d 100644 --- a/src/hyperlight_component_util/src/component.rs +++ b/src/hyperlight_component_util/src/component.rs @@ -149,6 +149,8 @@ pub fn read_component_single_exported_type<'a>( _ => {} } } + + // eprintln!("{:?}",ctx.types.into_iter().nth(n)); match last_idx { None => panic!("no exported type"), Some(n) => match ctx.types.into_iter().nth(n) { @@ -157,3 +159,92 @@ pub fn read_component_single_exported_type<'a>( }, } } + +pub fn read_component_specific_world_name<'a>( + items: impl Iterator>>, + world_name: String, +) -> Component<'a> { + let mut ctx = Ctx::new(None, false); + let mut world_idx = None; + + for x in items { + match x { + Ok(Version { num, encoding, .. }) => { + if encoding != wasmparser::Encoding::Component { + panic!("wasm file is not a component") + } + if num != 0xd { + panic!("unknown component encoding version 0x{:x}\n", num); + } + } + Ok(ComponentTypeSection(ts)) => { + for t in ts { + match t { + Ok(ComponentType::Component(ct)) => { + let ct_ = ctx.elab_component(&ct); + ctx.types.push(Defined::Component(ct_.unwrap())); + } + _ => panic!("non-component type"), + } + } + } + Ok(ComponentExportSection(es)) => { + for e in es { + match e { + Err(_) => panic!("invalid export section"), + Ok(ce) => { + if ce.kind == ComponentExternalKind::Type { + ctx.types.push(raw_type_export_type(&ctx, &ce).clone()); + match ce.name { + wasmparser::ComponentExportName(name) => { + if name.eq_ignore_ascii_case(&world_name) { + eprintln!("WE GOT IN = {}", name); + eprintln!("Found matching world: {} (looking for: {})", name, world_name); + world_idx = Some(ctx.types.len() - 1); + eprintln!("Found matching world: {} (looking for: {:?})", name, world_idx.clone()); + } + } + } + } + } + } + } + } + Ok(ComponentAliasSection(r#as)) => { + for a in r#as { + match a { + Ok(ComponentAlias::InstanceExport { + kind: ComponentExternalKind::Type, + .. + }) + | Ok(ComponentAlias::Outer { + kind: ComponentOuterAliasKind::Type, + .. + }) => { + panic!("Component outer type aliases are not supported") + } + // Anything else doesn't affect the index + // space that we are interested in, so we can + // safely ignore + _ => {} + } + } + } + + // No other component section should be terribly relevant + // for us. We would not generally expect to find them in + // a file that just represents a type like this, but it + // seems like there are/may be a whole bunch of debugging + // custom sections, etc that might show up, so for now + // let's just ignore anything. + _ => {} + } + } + match world_idx { + None => panic!("expected world name not found"), + Some(n) => match ctx.types.into_iter().nth(n) { + Some(Defined::Component(c)) => c, + _ => panic!("final export is not component"), + }, + } +} diff --git a/src/hyperlight_component_util/src/util.rs b/src/hyperlight_component_util/src/util.rs index 8f9b853ae..29d5a75ef 100644 --- a/src/hyperlight_component_util/src/util.rs +++ b/src/hyperlight_component_util/src/util.rs @@ -50,6 +50,39 @@ pub fn read_wit_type_from_file R>( cb(export.kebab_name.to_string(), ct) } +pub fn read_world_from_file_1 R>( + filename: impl AsRef, + world_name: Option, + mut cb: F, +) -> R { + let path = std::path::Path::new(&filename); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + eprintln!("PATH2 = {:?}", path); + let manifest_dir = std::path::Path::new(&manifest_dir); + let path = manifest_dir.join(path); + + let bytes = std::fs::read(path).unwrap(); + let i = wasmparser::Parser::new(0).parse_all(&bytes); + let ct = crate::component::read_component_specific_world_name(i, world_name.unwrap()); + + // because of the two-level encapsulation scheme, we need to look + // for the single export of the component type that we just read + if !ct.uvars.is_empty() + || !ct.imports.is_empty() + || !ct.instance.evars.is_empty() + || ct.instance.unqualified.exports.len() != 1 + { + panic!("malformed component type container for wit type"); + }; + let export = &ct.instance.unqualified.exports[0]; + use etypes::ExternDesc; + let ExternDesc::Component(ct) = &export.desc else { + panic!("malformed component type container: does not contain component type"); + }; + log::debug!("hcm: considering component type {:?}", ct); + cb(export.kebab_name.to_string(), ct) +} + /// Deal with `$HYPERLIGHT_COMPONENT_MACRO_DEBUG`: if it is present, /// save the given token stream (representing the result of /// macroexpansion) to the debug file and include that file instead of diff --git a/src/hyperlight_host/tests/host_bingen2_test.rs b/src/hyperlight_host/tests/host_bingen2_test.rs new file mode 100644 index 000000000..15687b0df --- /dev/null +++ b/src/hyperlight_host/tests/host_bingen2_test.rs @@ -0,0 +1,428 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ +#![allow(clippy::disallowed_macros)] + +use std::sync::{Arc, Mutex}; + +use hyperlight_common::resource::BorrowedResourceGuard; +use hyperlight_host::{GuestBinary, MultiUseSandbox, UninitializedSandbox}; +use hyperlight_testing::wit_guest_as_string; +use hyperlight_component_macro::host_bindgen2; +extern crate alloc; +mod bindings2 { + hyperlight_component_macro::host_bindgen2!({path: "../tests/rust_guests/witguest/interface.wasm", world_name: "test"}); +} + +use bindings2::test::wit::roundtrip::{Testrecord, Testvariant}; +use bindings2::*; + +impl PartialEq for Testrecord { + fn eq(&self, other: &Self) -> bool { + self.contents == other.contents && self.length == other.length + } +} + +impl PartialEq for Testvariant { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Testvariant::VariantA, Testvariant::VariantA) => true, + (Testvariant::VariantB(s1), Testvariant::VariantB(s2)) => s1 == s2, + (Testvariant::VariantC(c1), Testvariant::VariantC(c2)) => c1 == c2, + _ => false, + } + } +} + +impl Clone for Testrecord { + fn clone(&self) -> Self { + Self { + contents: self.contents.clone(), + length: self.length, + } + } +} + +impl Clone for Testvariant { + fn clone(&self) -> Self { + match self { + Self::VariantA => Self::VariantA, + Self::VariantB(s) => Self::VariantB(s.clone()), + Self::VariantC(c) => Self::VariantC(*c), + } + } +} + +struct Host {} + +impl test::wit::Roundtrip for Host { + fn roundtrip_bool(&mut self, x: bool) -> bool { + x + } + fn roundtrip_s8(&mut self, x: i8) -> i8 { + x + } + fn roundtrip_s16(&mut self, x: i16) -> i16 { + x + } + fn roundtrip_s32(&mut self, x: i32) -> i32 { + x + } + fn roundtrip_s64(&mut self, x: i64) -> i64 { + x + } + fn roundtrip_u8(&mut self, x: u8) -> u8 { + x + } + fn roundtrip_u16(&mut self, x: u16) -> u16 { + x + } + fn roundtrip_u32(&mut self, x: u32) -> u32 { + x + } + fn roundtrip_u64(&mut self, x: u64) -> u64 { + x + } + fn roundtrip_f32(&mut self, x: f32) -> f32 { + x + } + fn roundtrip_f64(&mut self, x: f64) -> f64 { + x + } + fn roundtrip_char(&mut self, x: char) -> char { + x + } + fn roundtrip_string(&mut self, x: alloc::string::String) -> alloc::string::String { + x + } + fn roundtrip_list(&mut self, x: alloc::vec::Vec) -> alloc::vec::Vec { + x + } + fn roundtrip_tuple(&mut self, x: (alloc::string::String, u8)) -> (alloc::string::String, u8) { + x + } + fn roundtrip_option( + &mut self, + x: ::core::option::Option, + ) -> ::core::option::Option { + x + } + fn roundtrip_result( + &mut self, + x: ::core::result::Result, + ) -> ::core::result::Result { + x + } + fn roundtrip_record( + &mut self, + x: test::wit::roundtrip::Testrecord, + ) -> test::wit::roundtrip::Testrecord { + x + } + fn roundtrip_flags_small( + &mut self, + x: test::wit::roundtrip::Smallflags, + ) -> test::wit::roundtrip::Smallflags { + x + } + fn roundtrip_flags_large( + &mut self, + x: test::wit::roundtrip::Largeflags, + ) -> test::wit::roundtrip::Largeflags { + x + } + fn roundtrip_variant( + &mut self, + x: test::wit::roundtrip::Testvariant, + ) -> test::wit::roundtrip::Testvariant { + x + } + fn roundtrip_enum( + &mut self, + x: test::wit::roundtrip::Testenum, + ) -> test::wit::roundtrip::Testenum { + x + } + fn roundtrip_fix_list(&mut self, x: [u8; 4]) -> [u8; 4] { + x + } + fn roundtrip_fix_list_u32(&mut self, x: [u32; 4]) -> [u32; 4] { + x + } + fn roundtrip_fix_list_u64(&mut self, x: [u64; 4]) -> [u64; 4] { + x + } + fn roundtrip_fix_list_i8(&mut self, x: [i8; 4]) -> [i8; 4] { + x + } + fn roundtrip_fix_list_i16(&mut self, x: [i16; 4]) -> [i16; 4] { + x + } + fn roundtrip_fix_list_i32(&mut self, x: [i32; 4]) -> [i32; 4] { + x + } + fn roundtrip_fix_list_i64(&mut self, x: [i64; 4]) -> [i64; 4] { + x + } + fn roundtrip_fix_list_f32(&mut self, x: [f32; 4]) -> [f32; 4] { + x + } + fn roundtrip_fix_list_f64(&mut self, x: [f64; 4]) -> [f64; 4] { + x + } + fn roundtrip_fix_list_u8_size8(&mut self, x: [u8; 8]) -> [u8; 8] { + x + } + fn roundtrip_fix_list_u64_size2(&mut self, x: [u64; 2]) -> [u64; 2] { + x + } + fn roundtrip_fix_list_string(&mut self, x: [String; 4]) -> [String; 4] { + x + } + fn roundtrip_fix_array_of_lists(&mut self, x: [Vec; 3]) -> [Vec; 3] { + x + } + fn roundtrip_fix_array_of_string_lists(&mut self, x: [Vec; 2]) -> [Vec; 2] { + x + } + + fn roundtrip_no_result(&mut self, _x: u32) {} +} + +struct TestResource { + n_calls: u32, + x: String, + last: char, +} +impl TestResource { + fn new(x: String, last: char) -> Arc> { + Arc::new(Mutex::new(TestResource { + n_calls: 0, + x, + last, + })) + } +} + +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; +// We use some care below in the tests that use HAS_BEEN_DROPPED to +// synchronise on this mutex to avoid them stepping on each other +static SERIALIZE_TEST_RESOURCE_TESTS: Mutex<()> = Mutex::new(()); +static HAS_BEEN_DROPPED: AtomicBool = AtomicBool::new(false); + +impl Drop for TestResource { + fn drop(&mut self) { + assert_eq!(self.x, "strabc"); + assert_eq!(self.last, 'c'); + assert!(!HAS_BEEN_DROPPED.swap(true, Relaxed)); + } +} + +impl test::wit::host_resource::Testresource for Host { + type T = Arc>; + fn new(&mut self, x: String, last: char) -> Self::T { + TestResource::new(x, last) + } + fn append_char(&mut self, self_: BorrowedResourceGuard<'_, Self::T>, c: char) { + let mut self_ = self_.lock().unwrap(); + match self_.n_calls { + // These line up to the initial values and calls made by + // witguest.rs. Mostly, this just checks that (even after + // round-tripping an owned reference through the host), we + // do always seem to get the correct structure. + 0 => { + assert_eq!(self_.x, "str"); + assert_eq!(self_.last, 'z'); + } + 1 => { + assert_eq!(self_.x, "stra"); + assert_eq!(self_.last, 'a'); + } + 2 => { + assert_eq!(self_.x, "strab"); + assert_eq!(self_.last, 'b'); + } + _ => panic!(), + }; + self_.n_calls += 1; + self_.x.push(c); + self_.last = c; + } +} + +impl test::wit::HostResource for Host { + fn roundtrip_own(&mut self, owned: Arc>) -> Arc> { + owned + } + + fn return_own(&mut self, _: Arc>) { + // Not much to do here other than let it be dropped + } +} + +#[allow(refining_impl_trait)] +impl test::wit::TestImports for Host { + type Roundtrip = Self; + fn roundtrip(&mut self) -> &mut Self { + self + } + type HostResource = Self; + fn host_resource(&mut self) -> &mut Self { + self + } +} + +fn sb() -> TestSandbox { + let path = wit_guest_as_string().unwrap(); + let guest_path = GuestBinary::FilePath(path); + let uninit = UninitializedSandbox::new(guest_path, None).unwrap(); + test::wit::Test::instantiate(uninit, Host {}) +} + +mod host_bindgen2_test { + + use proptest::prelude::*; + + use crate::bindings2::test::wit::{Roundtrip, TestExports, TestHostResource, roundtrip}; + use crate::sb; + + prop_compose! { + fn arb_testrecord()(contents in ".*", length in any::()) -> roundtrip::Testrecord { + roundtrip::Testrecord { contents, length } + } + } + + prop_compose! { + fn arb_smallflags()(flag_a: bool, flag_b: bool, flag_c: bool) -> roundtrip::Smallflags { + roundtrip::Smallflags { flag_a, flag_b, flag_c } + } + } + + prop_compose! { + fn arb_largeflags()( + flag00: bool, flag01: bool, flag02: bool, flag03: bool, flag04: bool, flag05: bool, flag06: bool, flag07: bool, + flag08: bool, flag09: bool, flag0a: bool, flag0b: bool, flag0c: bool, flag0d: bool, flag0e: bool, flag0f: bool, + + flag10: bool, flag11: bool, flag12: bool, flag13: bool, flag14: bool, flag15: bool, flag16: bool, flag17: bool, + flag18: bool, flag19: bool, flag1a: bool, flag1b: bool, flag1c: bool, flag1d: bool, flag1e: bool, flag1f: bool, + ) -> roundtrip::Largeflags { + roundtrip::Largeflags { + flag00, flag01, flag02, flag03, flag04, flag05, flag06, flag07, + flag08, flag09, flag0a, flag0b, flag0c, flag0d, flag0e, flag0f, + + flag10, flag11, flag12, flag13, flag14, flag15, flag16, flag17, + flag18, flag19, flag1a, flag1b, flag1c, flag1d, flag1e, flag1f, + } + } + } + + fn arb_testvariant() -> impl Strategy { + use roundtrip::Testvariant::*; + prop_oneof![ + Just(VariantA), + any::().prop_map(VariantB), + any::().prop_map(VariantC), + ] + } + + fn arb_testenum() -> impl Strategy { + use roundtrip::Testenum::*; + prop_oneof![Just(EnumA), Just(EnumB), Just(EnumC),] + } + + macro_rules! make_test { + ($fn:ident, $($ty:tt)*) => { + proptest! { + #[test] + fn $fn(x $($ty)*) { + assert_eq!(x, sb().roundtrip().$fn(x.clone())) + } + } + } + } + + make_test! { roundtrip_bool, : bool } + make_test! { roundtrip_u8, : u8 } + make_test! { roundtrip_u16, : u16 } + make_test! { roundtrip_u32, : u32 } + make_test! { roundtrip_u64, : u64 } + make_test! { roundtrip_s8, : i8 } + make_test! { roundtrip_s16, : i16 } + make_test! { roundtrip_s32, : i32 } + make_test! { roundtrip_s64, : i64 } + make_test! { roundtrip_f32, : f32 } + make_test! { roundtrip_f64, : f64 } + make_test! { roundtrip_char, : char } + make_test! { roundtrip_string, : String } + + make_test! { roundtrip_list, : Vec } + make_test! { roundtrip_tuple, : (String, u8) } + make_test! { roundtrip_option, : Option } + make_test! { roundtrip_result, : Result } + + make_test! { roundtrip_record, in arb_testrecord() } + make_test! { roundtrip_flags_small, in arb_smallflags() } + make_test! { roundtrip_flags_large, in arb_largeflags() } + make_test! { roundtrip_variant, in arb_testvariant() } + make_test! { roundtrip_enum, in arb_testenum() } + make_test! { roundtrip_fix_list, : [u8; 4] } + make_test! { roundtrip_fix_list_u32, : [u32; 4] } + make_test! { roundtrip_fix_list_u64, : [u64; 4] } + make_test! { roundtrip_fix_list_i8, : [i8; 4] } + make_test! { roundtrip_fix_list_i16, : [i16; 4] } + make_test! { roundtrip_fix_list_i32, : [i32; 4] } + make_test! { roundtrip_fix_list_i64, : [i64; 4] } + make_test! { roundtrip_fix_list_f32, : [f32; 4] } + make_test! { roundtrip_fix_list_f64, : [f64; 4] } + make_test! { roundtrip_fix_list_u8_size8, : [u8; 8] } + make_test! { roundtrip_fix_list_u64_size2, : [u64; 2] } + make_test! { roundtrip_fix_list_string, : [String; 4] } + make_test! { roundtrip_fix_array_of_lists, : [Vec; 3] } + make_test! { roundtrip_fix_array_of_string_lists, : [Vec; 2] } + + #[test] + fn test_roundtrip_no_result() { + sb().roundtrip().roundtrip_no_result(42); + } + + use std::sync::atomic::Ordering::Relaxed; + + #[test] + fn test_host_resource_uses_locally() { + let guard = crate::SERIALIZE_TEST_RESOURCE_TESTS.lock(); + crate::HAS_BEEN_DROPPED.store(false, Relaxed); + { + sb().test_host_resource().test_uses_locally(); + } + assert!(crate::HAS_BEEN_DROPPED.load(Relaxed)); + drop(guard); + } + #[test] + fn test_host_resource_passed_in_out() { + let guard = crate::SERIALIZE_TEST_RESOURCE_TESTS.lock(); + crate::HAS_BEEN_DROPPED.store(false, Relaxed); + { + 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(); + } + assert!(crate::HAS_BEEN_DROPPED.load(Relaxed)); + drop(guard); + } +}