From 68d27f34936ef0bebcf6396e9c41b5cae893de0c Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Fri, 26 Jun 2026 12:56:19 +0200 Subject: [PATCH 1/3] feat: support WIT source inputs in component bindgen macro Add explicit wit, wasm, and inline input forms for component bindgen macros. Signed-off-by: Tomasz Andrzejak --- .github/workflows/dep_build_guests.yml | 27 ---- .github/workflows/dep_build_test.yml | 18 --- Cargo.lock | 80 +++++++++-- Justfile | 24 ++-- src/hyperlight_component_macro/src/lib.rs | 111 ++++++++++------ src/hyperlight_component_util/Cargo.toml | 2 + src/hyperlight_component_util/src/util.rs | 85 ++++++++++-- .../tests/wasmtime_guest_codegen.rs | 125 +++++++++++++++++- src/hyperlight_host/tests/wit_test.rs | 46 ++++++- src/tests/rust_guests/Cargo.lock | 74 +++++++++++ .../rust_guests/witguest/src/bindings.rs | 2 +- 11 files changed, 467 insertions(+), 127 deletions(-) diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index 071dab8a9..7461ac007 100644 --- a/.github/workflows/dep_build_guests.yml +++ b/.github/workflows/dep_build_guests.yml @@ -109,30 +109,3 @@ jobs: path: src/tests/c_guests/bin/${{ inputs.config }}/ retention-days: 1 if-no-files-found: error - - - name: Upload interface.wasm - if: inputs.config == 'debug' && inputs.arch == 'X64' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: interface-wasm - path: src/tests/rust_guests/witguest/interface.wasm - retention-days: 1 - if-no-files-found: error - - - name: Upload twoworlds.wasm - if: inputs.config == 'debug' && inputs.arch == 'X64' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: twoworlds-wasm - path: src/tests/rust_guests/witguest/twoworlds.wasm - retention-days: 1 - if-no-files-found: error - - - name: Upload bindgen-test-cases.wasm - if: inputs.config == 'debug' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: bindgen-test-cases-wasm - path: src/tests/rust_guests/witguest/bindgen-test-cases.wasm - retention-days: 1 - if-no-files-found: error diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index 834347960..135338892 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -83,24 +83,6 @@ jobs: name: c-guests-${{ inputs.arch }}-${{ inputs.config }} path: src/tests/c_guests/bin/${{ inputs.config }}/ - - name: Download interface.wasm - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: interface-wasm - path: src/tests/rust_guests/witguest/ - - - name: Download twoworlds.wasm - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: twoworlds-wasm - path: src/tests/rust_guests/witguest/ - - - name: Download bindgen-test-cases.wasm - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: bindgen-test-cases-wasm - path: src/tests/rust_guests/witguest/ - - name: Build run: just build ${{ inputs.config }} diff --git a/Cargo.lock b/Cargo.lock index 42d7f4139..541d52fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1659,6 +1659,8 @@ dependencies = [ "syn", "tracing", "wasmparser 0.252.0", + "wit-component 0.252.0", + "wit-parser 0.252.0", ] [[package]] @@ -4155,9 +4157,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -4368,6 +4370,16 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wasm-encoder" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +dependencies = [ + "leb128fmt", + "wasmparser 0.252.0", +] + [[package]] name = "wasm-metadata" version = "0.244.0" @@ -4376,10 +4388,22 @@ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", + "wasm-encoder 0.244.0", "wasmparser 0.244.0", ] +[[package]] +name = "wasm-metadata" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e08e02a3cd55bf778009d4cd6faae50da011f293644daf78a531a32d6d142" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -4619,7 +4643,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.244.0", ] [[package]] @@ -4633,9 +4657,9 @@ dependencies = [ "indexmap", "prettyplease", "syn", - "wasm-metadata", + "wasm-metadata 0.244.0", "wit-bindgen-core", - "wit-component", + "wit-component 0.244.0", ] [[package]] @@ -4666,10 +4690,29 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", + "wasm-encoder 0.244.0", + "wasm-metadata 0.244.0", "wasmparser 0.244.0", - "wit-parser", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-component" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76db0662b590f45d33d0e363fa13539a5a1eecd35d5a12fe208c335461c1053d" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.252.0", + "wasm-metadata 0.252.0", + "wasmparser 0.252.0", + "wit-parser 0.252.0", ] [[package]] @@ -4690,6 +4733,25 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wit-parser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" +dependencies = [ + "anyhow", + "hashbrown 0.17.0", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.252.0", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Justfile b/Justfile index 8011a3913..d8d8c65a5 100644 --- a/Justfile +++ b/Justfile @@ -61,13 +61,7 @@ guests: build-and-move-rust-guests build-and-move-c-guests ensure-cargo-hyperlight: {{ if os() == "windows" { "if (-not ((cargo hyperlight --version 2>$null) -like '*" + cargo-hyperlight-version + "*')) { cargo install --locked --force --version " + cargo-hyperlight-version + " cargo-hyperlight }" } else { "cargo hyperlight --version 2>/dev/null | grep -qF '" + cargo-hyperlight-version + "' || cargo install --locked --force --version " + cargo-hyperlight-version + " cargo-hyperlight" } }} -witguest-wit: - cargo install --locked wasm-tools - cd src/tests/rust_guests/witguest && wasm-tools component wit guest.wit -w -o interface.wasm - cd src/tests/rust_guests/witguest && wasm-tools component wit two_worlds.wit -w -o twoworlds.wasm - cd src/tests/rust_guests/witguest && wasm-tools component wit bindgen-test-cases/ -w -o bindgen-test-cases.wasm - -build-rust-guests target=default-target features="": (witguest-wit) (ensure-cargo-hyperlight) +build-rust-guests target=default-target features="": (ensure-cargo-hyperlight) @# --workspace unifies feature resolution so shared deps build once. Needed because witguest @# pulls bindgen via hyperlight-component-macro, which would otherwise turn on extra features @# on libc's build.rs host deps and force a libc rebuild. simple/dummyguest don't hit this. @@ -86,7 +80,7 @@ clean: clean-rust clean-rust: cargo clean cd src/tests/rust_guests && cargo clean - {{ if os() == "windows" { "Remove-Item src/tests/rust_guests/witguest/interface.wasm -Force -ErrorAction SilentlyContinue" } else { "rm -f src/tests/rust_guests/witguest/interface.wasm" } }} + {{ if os() == "windows" { "Remove-Item src/tests/rust_guests/witguest/*.wasm -Force -ErrorAction SilentlyContinue" } else { "rm -f src/tests/rust_guests/witguest/*.wasm" } }} git clean -fdx src/tests/c_guests/bin src/tests/rust_guests/bin ################ @@ -247,11 +241,11 @@ test-isolated target=default-target features="" : {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact # runs integration tests -test-integration target=default-target features="": (witguest-wit) +test-integration target=default-target features="": @# run execute_on_heap test with feature "executable_heap" on (runs with off during normal tests) {{ cargo-cmd }} test {{ if features =="" {"--features executable_heap"} else if features=="no-default-features" {"--no-default-features --features executable_heap"} else {"--no-default-features -F executable_heap," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap - @# run component-util integration tests that depend on generated WIT inputs + @# run component-util integration tests that exercise WIT input codegen {{ cargo-cmd }} test -p hyperlight-component-util --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test wasmtime_guest_codegen @# run the rest of the integration tests @@ -334,14 +328,14 @@ fmt-apply: (ensure-nightly-fmt) cargo +{{nightly-toolchain}} fmt --manifest-path src/tests/rust_guests/Cargo.toml --all cargo +{{nightly-toolchain}} fmt --manifest-path src/hyperlight_guest_capi/Cargo.toml -clippy target=default-target: (witguest-wit) +clippy target=default-target: {{ cargo-cmd }} clippy --all-targets {{ if hyperlight-target-arch == "x86_64" { "--all-features" } else { "" } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -- -D warnings # for use on a linux host-machine when cross-compiling to windows. Uses the windows-gnu which should be sufficient for most purposes -clippyw target=default-target: (witguest-wit) +clippyw target=default-target: {{ cargo-cmd }} clippy --all-targets --all-features --target x86_64-pc-windows-gnu --profile={{ if target == "debug" { "dev" } else { target } }} -- -D warnings -clippy-guests target=default-target: (witguest-wit) (ensure-cargo-hyperlight) +clippy-guests target=default-target: (ensure-cargo-hyperlight) cd src/tests/rust_guests && cargo hyperlight clippy --workspace --profile={{ if target == "debug" { "dev" } else { target } }} -- -D warnings clippy-apply-fix-unix: @@ -351,7 +345,7 @@ clippy-apply-fix-windows: cargo clippy --target x86_64-pc-windows-msvc --fix --all # Run clippy with feature combinations for all packages -clippy-exhaustive target=default-target: (witguest-wit) +clippy-exhaustive target=default-target: ./hack/clippy-package-features.sh hyperlight-host {{ target }} {{ target-triple }} ./hack/clippy-package-features.sh hyperlight-guest {{ target }} ./hack/clippy-package-features.sh hyperlight-guest-bin {{ target }} @@ -364,7 +358,7 @@ clippy-exhaustive target=default-target: (witguest-wit) just clippy-guests {{ target }} # Test a specific package with all feature combinations -clippy-package package target=default-target: (witguest-wit) +clippy-package package target=default-target: ./hack/clippy-package-features.sh {{ package }} {{ target }} # Verify Minimum Supported Rust Version diff --git a/src/hyperlight_component_macro/src/lib.rs b/src/hyperlight_component_macro/src/lib.rs index 94a312448..76c14d8a7 100644 --- a/src/hyperlight_component_macro/src/lib.rs +++ b/src/hyperlight_component_macro/src/lib.rs @@ -20,19 +20,19 @@ limitations under the License. //! (e.g. those described by WIT) to describe the interface between a //! Hyperlight host and guest. //! -//! For both host and guest bindings, bindings generation takes in a -//! binary-encoded wasm component, which should have roughly the +//! For both host and guest bindings, bindings generation takes in WIT +//! source input (`*.wit` files or WIT package directories) or a +//! wasm-encoded WIT package. Wasm input should have roughly the //! structure of a binary-encoded WIT (in particular, component //! import/export kebab-names should have `wit:package/name` namespace //! structure, and the same two-level convention for wrapping a -//! component type into an actual component should be adhered to). If -//! you are using WIT as the input, it is easy to build such a file -//! via `wasm-tools component wit -w -o file.wasm file.wit`. +//! component type into an actual component should be adhered to). //! -//! Both macros can take the path to such a file as a parameter, or, -//! if one is not provided, will fall back to using the path in the -//! environment variable `$WIT_WORLD`. A relative path provided either way -//! will be resolved relative to `$CARGO_MANIFEST_DIR`. +//! Both macros can take explicit `wit:`, `wasm:`, or `inline:` inputs. WIT file +//! paths may also be WIT package directories with `deps/`; inline WIT does not +//! resolve external dependencies. For compatibility, a bare string literal, +//! `path:` option, or `$WIT_WORLD` is treated as wasm-encoded WIT. Relative paths +//! are resolved relative to `$CARGO_MANIFEST_DIR`. //! //! ## Debugging //! @@ -53,7 +53,7 @@ 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 +/// Create host bindings for the WIT world or wasm component type in the file /// passed in (or `$WIT_WORLD`, if nothing is passed in). This will /// produce all relevant types and trait implementations for the /// component type, as well as functions allowing the component to be @@ -69,13 +69,10 @@ use syn::{Ident, LitStr, Result, Token}; pub fn host_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let _ = env_logger::try_init(); let parsed_bindgen_input = syn::parse_macro_input!(input as BindgenInputParams); - let path = match parsed_bindgen_input.path { - Some(path_env) => path_env.into_os_string(), - None => std::env::var_os("WIT_WORLD").unwrap(), - }; - let world_name = parsed_bindgen_input.world_name; + let BindgenInputParams { world_name, source } = parsed_bindgen_input; + let source = source_or_env(source); - util::read_wit_type_from_file(path, world_name, |kebab_name, ct| { + util::read_wit_type(source, world_name, |kebab_name, ct| { let decls = emit::run_state(false, false, |s| { rtypes::emit_toplevel(s, &kebab_name, ct); host::emit_toplevel(s, &kebab_name, ct); @@ -85,7 +82,7 @@ pub fn host_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream { } /// Create the hyperlight_guest_init() function (which should be -/// called in hyperlight_main()) for the wasm component type in the +/// called in hyperlight_main()) for the WIT world or wasm component type in the /// file passed in (or `$WIT_WORLD`, if nothing is passed in). This /// function registers Hyperlight functions for component exports /// (which are implemented by calling into the trait provided) and @@ -95,13 +92,10 @@ pub fn host_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn guest_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let _ = env_logger::try_init(); let parsed_bindgen_input = syn::parse_macro_input!(input as BindgenInputParams); - let path = match parsed_bindgen_input.path { - Some(path_env) => path_env.into_os_string(), - None => std::env::var_os("WIT_WORLD").unwrap(), - }; - let world_name = parsed_bindgen_input.world_name; + let BindgenInputParams { world_name, source } = parsed_bindgen_input; + let source = source_or_env(source); - util::read_wit_type_from_file(path, world_name, |kebab_name, ct| { + util::read_wit_type(source, world_name, |kebab_name, ct| { let decls = emit::run_state(true, false, |s| { // Emit type/trait definitions for all instances in the world rtypes::emit_toplevel(s, &kebab_name, ct); @@ -119,12 +113,46 @@ pub fn guest_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream #[derive(Debug)] struct BindgenInputParams { world_name: Option, - path: Option, + source: Option, +} + +fn source_or_env(source: Option) -> util::WitSource { + source.unwrap_or_else(|| { + util::WitSource::Wasm(std::path::PathBuf::from( + std::env::var_os("WIT_WORLD").unwrap(), + )) + }) +} + +fn unknown_key_error(key: &Ident) -> syn::Error { + syn::Error::new( + key.span(), + format!( + "unknown parameter '{}'; expected 'path', 'wit', 'wasm', 'inline', 'world', or 'world_name'", + key + ), + ) +} + +fn source_from_key(key: &Ident, value: LitStr) -> Result { + match key.to_string().as_str() { + "path" => Ok(util::WitSource::Wasm(std::path::PathBuf::from( + value.value(), + ))), + "wit" => Ok(util::WitSource::Wit(std::path::PathBuf::from( + value.value(), + ))), + "wasm" => Ok(util::WitSource::Wasm(std::path::PathBuf::from( + value.value(), + ))), + "inline" => Ok(util::WitSource::Inline(value.value())), + _ => Err(unknown_key_error(key)), + } } impl Parse for BindgenInputParams { fn parse(input: ParseStream) -> Result { - let mut path = None; + let mut source = None; let mut world_name = None; if input.peek(syn::token::Brace) { @@ -137,35 +165,40 @@ impl Parse for BindgenInputParams { content.parse::()?; match key.to_string().as_str() { - "world_name" => { + "world" | "world_name" => { let value: LitStr = content.parse()?; world_name = Some(value.value()); } - "path" => { + "path" | "wit" | "wasm" | "inline" => { let value: LitStr = content.parse()?; - path = Some(std::path::PathBuf::from(value.value())); - } - _ => { - return Err(syn::Error::new( - key.span(), - format!( - "unknown parameter '{}'; expected 'path' or 'world_name'", - key - ), - )); + if source.is_some() { + return Err(syn::Error::new( + key.span(), + "only one input source may be specified", + )); + } + source = Some(source_from_key(&key, value)?); } + _ => return Err(unknown_key_error(&key)), } // Parse optional comma if content.peek(Token![,]) { content.parse::()?; } } + } else if input.peek(Ident) { + let key: Ident = input.parse()?; + input.parse::()?; + let value: LitStr = input.parse()?; + source = Some(source_from_key(&key, value)?); } else { let option_path_litstr = input.parse::>()?; if let Some(concrete_path) = option_path_litstr { - path = Some(std::path::PathBuf::from(concrete_path.value())); + source = Some(util::WitSource::Wasm(std::path::PathBuf::from( + concrete_path.value(), + ))); } } - Ok(Self { world_name, path }) + Ok(Self { world_name, source }) } } diff --git a/src/hyperlight_component_util/Cargo.toml b/src/hyperlight_component_util/Cargo.toml index 5f8d2b184..37e1276fa 100644 --- a/src/hyperlight_component_util/Cargo.toml +++ b/src/hyperlight_component_util/Cargo.toml @@ -16,6 +16,8 @@ name = "hyperlight_component_util" [dependencies] wasmparser = { version = "0.252.0" } +wit-component = { version = "0.252.0" } +wit-parser = { version = "0.252.0" } quote = { version = "1.0.45" } proc-macro2 = { version = "1.0.106" } syn = { version = "2.0.117" } diff --git a/src/hyperlight_component_util/src/util.rs b/src/hyperlight_component_util/src/util.rs index 8e1960a30..4b23db88f 100644 --- a/src/hyperlight_component_util/src/util.rs +++ b/src/hyperlight_component_util/src/util.rs @@ -17,19 +17,73 @@ limitations under the License. //! General utilities for bindgen macros use crate::etypes; -/// Read and parse a WIT type encapsulated in a wasm file from the -/// given filename, relative to the cargo manifest directory. -pub fn read_wit_type_from_file R>( - filename: impl AsRef, +/// Input accepted by component bindgen. +#[derive(Debug)] +pub enum WitSource { + Wasm(std::path::PathBuf), + Wit(std::path::PathBuf), + Inline(String), +} + +impl WitSource { + fn encode(self) -> Vec { + match self { + Self::Wasm(path) => { + let path = manifest_path(&path); + let bytes = std::fs::read(&path).unwrap_or_else(|err| { + panic!( + "failed to read wasm-encoded WIT input '{}': {err}", + path.display() + ) + }); + if !wasmparser::Parser::is_component(&bytes) { + panic!( + "wasm-encoded WIT input '{}' is not a wasm component", + path.display() + ); + } + bytes + } + Self::Wit(path) => { + let path = manifest_path(&path); + let mut resolve = wit_parser::Resolve::default(); + let (package, _) = resolve.push_path(&path).unwrap_or_else(|err| { + panic!("failed to parse WIT input '{}': {err:#}", path.display()) + }); + + wit_component::encode(&resolve, package).unwrap_or_else(|err| { + panic!( + "failed to encode WIT input '{}' as a wasm component type: {err:#}", + path.display() + ) + }) + } + Self::Inline(contents) => { + let mut resolve = wit_parser::Resolve::default(); + let package = resolve + .push_str("inline.wit", &contents) + .unwrap_or_else(|err| panic!("failed to parse inline WIT input: {err:#}")); + + wit_component::encode(&resolve, package).unwrap_or_else(|err| { + panic!("failed to encode inline WIT input as a wasm component type: {err:#}") + }) + } + } + } +} + +fn manifest_path(path: &std::path::Path) -> std::path::PathBuf { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + std::path::Path::new(&manifest_dir).join(path) +} + +/// Read and parse a WIT type from a supported bindgen input. +pub fn read_wit_type R>( + source: WitSource, world_name: Option, mut cb: F, ) -> R { - let path = std::path::Path::new(&filename); - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let manifest_dir = std::path::Path::new(&manifest_dir); - let path = manifest_dir.join(path); - - let bytes = std::fs::read(path).unwrap(); + let bytes = source.encode(); let i = wasmparser::Parser::new(0).parse_all(&bytes); let ct = crate::component::read_component_single_exported_type(i, world_name); @@ -51,6 +105,17 @@ pub fn read_wit_type_from_file R>( cb(export.kebab_name.to_string(), ct) } +/// Read and parse a wasm-encoded WIT file, relative to the cargo manifest +/// directory. +pub fn read_wit_type_from_file R>( + filename: impl AsRef, + world_name: Option, + cb: F, +) -> R { + let src = WitSource::Wasm(std::path::PathBuf::from(filename.as_ref())); + read_wit_type(src, world_name, cb) +} + /// 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 then return the token stream diff --git a/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs b/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs index a9754c8ef..a62f09840 100644 --- a/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs +++ b/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs @@ -14,12 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ +use std::path::{Path, PathBuf}; + +use hyperlight_component_util::etypes::{ + Defined, ExternDesc, Handleable, ImportExport, TypeBound, Tyvar, Value, +}; use hyperlight_component_util::{emit, guest, rtypes, util}; +fn fixture_path(path: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join(path) +} + +fn encode_wit_fixture_to_wasm(path: &Path) -> PathBuf { + let mut resolve = wit_parser::Resolve::default(); + let (package, _) = resolve + .push_path(path) + .expect("WIT fixture should parse successfully"); + let wasm = wit_component::encode(&resolve, package).expect("WIT fixture should encode"); + let wasm_path = std::env::temp_dir().join(format!( + "hyperlight-component-util-{}-wit-fixture.wasm", + std::process::id() + )); + std::fs::write(&wasm_path, wasm).expect("temporary wasm fixture should be written"); + wasm_path +} + #[test] fn wasmtime_guest_codegen_emits_wasmtime_flags_macro() { - let generated = util::read_wit_type_from_file( - "../tests/rust_guests/witguest/interface.wasm", + let generated = util::read_wit_type( + util::WitSource::Wit(PathBuf::from("../tests/rust_guests/witguest/guest.wit")), None, |kebab_name, ct| { // Mirrors the hyperlight-wasm guest-bindgen expansion path: @@ -45,3 +68,101 @@ fn wasmtime_guest_codegen_emits_wasmtime_flags_macro() { assert!(!generated.contains("pub flag_b: bool")); assert!(!generated.contains("pub flag_c: bool")); } + +#[test] +fn read_wit_type_accepts_wasm_encoded_wit() { + let wasm_path = + encode_wit_fixture_to_wasm(&fixture_path("../tests/rust_guests/witguest/guest.wit")); + let kebab_name = util::read_wit_type( + util::WitSource::Wasm(wasm_path.clone()), + None, + |kebab_name, ct| { + assert!(!ct.imports.is_empty() || !ct.instance.unqualified.exports.is_empty()); + kebab_name + }, + ); + std::fs::remove_file(wasm_path).expect("temporary wasm fixture should be removed"); + + assert_eq!(kebab_name, "test:wit/test"); +} + +#[test] +fn read_wit_type_resolves_wit_package_deps() { + util::read_wit_type( + util::WitSource::Wit(PathBuf::from("../tests/rust_guests/witguest/with-deps")), + None, + |kebab_name, ct| { + assert_eq!(kebab_name, "test:with-deps/with-deps"); + let shared_types = ct + .imports + .iter() + .find(|import| import.kebab_name == "deps:shared/types") + .expect("dependency interface should be imported"); + + let ExternDesc::Instance(shared_types) = &shared_types.desc else { + panic!("dependency import should be an interface instance"); + }; + let dep_record = shared_types + .exports + .iter() + .find(|export| export.kebab_name == "dep-record") + .expect("dependency interface type should be resolved"); + let ExternDesc::Type(Defined::Handleable(Handleable::Var(Tyvar::Bound(0)))) = + &dep_record.desc + else { + panic!("dependency type should resolve to an imported type variable"); + }; + let TypeBound::Eq(Defined::Value(Value::Record(fields))) = &ct.uvars[0].bound else { + panic!("dependency type variable should be bound to the resolved record"); + }; + assert_eq!( + ct.uvars[0].origin.path.as_deref(), + Some( + [ + ImportExport::Export("dep-record"), + ImportExport::Import("deps:shared/types") + ] + .as_slice() + ) + ); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].name.name, "value"); + assert!(matches!(fields[0].ty, Value::String)); + }, + ); +} + +#[test] +fn read_wit_type_accepts_inline_wit() { + let kebab_name = util::read_wit_type( + util::WitSource::Inline( + r#" + package test:inline-bindgen; + + world inline-world { + export types; + } + + interface types { + record inline-record { + value: string, + } + } + "# + .to_string(), + ), + Some("inline-world".to_string()), + |kebab_name, ct| { + assert!( + ct.instance + .unqualified + .exports + .iter() + .any(|export| export.kebab_name == "test:inline-bindgen/types") + ); + kebab_name + }, + ); + + assert_eq!(kebab_name, "test:inline-bindgen/inline-world"); +} diff --git a/src/hyperlight_host/tests/wit_test.rs b/src/hyperlight_host/tests/wit_test.rs index b0ccb5ae6..1839c9236 100644 --- a/src/hyperlight_host/tests/wit_test.rs +++ b/src/hyperlight_host/tests/wit_test.rs @@ -22,7 +22,7 @@ use hyperlight_testing::wit_guest_as_string; extern crate alloc; mod bindings { - hyperlight_component_macro::host_bindgen!("../tests/rust_guests/witguest/interface.wasm"); + hyperlight_component_macro::host_bindgen!(wit: "../tests/rust_guests/witguest/guest.wit"); } use bindings::test::wit::roundtrip::{Testrecord, Testvariant}; @@ -427,7 +427,10 @@ mod wit_test { } mod pick_world_bindings { - hyperlight_component_macro::host_bindgen!({path: "../tests/rust_guests/witguest/twoworlds.wasm", world_name: "firstworld"}); + hyperlight_component_macro::host_bindgen!({ + wit: "../tests/rust_guests/witguest/two_worlds.wit", + world: "firstworld", + }); } mod pick_world_binding_test { use crate::pick_world_bindings::r#twoworlds::r#wit::r#first_import::RecFirstImport; @@ -450,7 +453,10 @@ mod pick_world_binding_test { } mod pick_world_bindings2 { - hyperlight_component_macro::host_bindgen!({path: "../tests/rust_guests/witguest/twoworlds.wasm", world_name: "secondworld"}); + hyperlight_component_macro::host_bindgen!({ + wit: "../tests/rust_guests/witguest/two_worlds.wit", + world: "secondworld", + }); } mod pick_world_binding_test2 { use crate::pick_world_bindings2::r#twoworlds::r#wit::r#second_export::RecSecondExport; @@ -473,9 +479,7 @@ mod pick_world_binding_test2 { } mod bindgen_test_case_bindings { - hyperlight_component_macro::host_bindgen!( - "../tests/rust_guests/witguest/bindgen-test-cases.wasm" - ); + hyperlight_component_macro::host_bindgen!(wit: "../tests/rust_guests/witguest/bindgen-test-cases"); } mod bindgen_test_cases { use crate::bindgen_test_case_bindings::*; @@ -537,3 +541,33 @@ mod bindgen_test_cases { } } } + +mod inline_bindings { + hyperlight_component_macro::host_bindgen!({ + inline: r#" + package test:inline-bindgen; + + world inline-world { + export types; + } + + interface types { + record inline-record { + value: string, + } + } + "#, + world: "inline-world", + }); +} +mod inline_bindgen_test { + use crate::inline_bindings::test::inline_bindgen::types::InlineRecord; + + #[test] + fn inline_wit_types_are_generated() { + let result = InlineRecord { + value: String::from("inline"), + }; + assert_eq!(result.value, "inline"); + } +} diff --git a/src/tests/rust_guests/Cargo.lock b/src/tests/rust_guests/Cargo.lock index 2d6177c69..7e833446e 100644 --- a/src/tests/rust_guests/Cargo.lock +++ b/src/tests/rust_guests/Cargo.lock @@ -231,6 +231,8 @@ dependencies = [ "syn", "tracing", "wasmparser", + "wit-component", + "wit-parser", ] [[package]] @@ -281,6 +283,12 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.14.0" @@ -338,6 +346,12 @@ dependencies = [ "syn", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "linkme" version = "0.3.36" @@ -679,6 +693,28 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasm-encoder" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e08e02a3cd55bf778009d4cd6faae50da011f293644daf78a531a32d6d142" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasmparser" version = "0.252.0" @@ -716,6 +752,44 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-component" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76db0662b590f45d33d0e363fa13539a5a1eecd35d5a12fe208c335461c1053d" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + [[package]] name = "witguest" version = "0.10.0" diff --git a/src/tests/rust_guests/witguest/src/bindings.rs b/src/tests/rust_guests/witguest/src/bindings.rs index 2316def29..2d8c389c2 100644 --- a/src/tests/rust_guests/witguest/src/bindings.rs +++ b/src/tests/rust_guests/witguest/src/bindings.rs @@ -16,4 +16,4 @@ limitations under the License. extern crate alloc; -hyperlight_component_macro::guest_bindgen!("interface.wasm"); +hyperlight_component_macro::guest_bindgen!(wit: "guest.wit"); From ecea211119b16c2da3bbe593bdc2475716fdd0ee Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Fri, 26 Jun 2026 13:11:45 +0200 Subject: [PATCH 2/3] fix: address copilot review Signed-off-by: Tomasz Andrzejak --- src/hyperlight_component_macro/src/lib.rs | 8 +++++++- .../tests/wasmtime_guest_codegen.rs | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/hyperlight_component_macro/src/lib.rs b/src/hyperlight_component_macro/src/lib.rs index 76c14d8a7..52a327f0f 100644 --- a/src/hyperlight_component_macro/src/lib.rs +++ b/src/hyperlight_component_macro/src/lib.rs @@ -190,7 +190,13 @@ impl Parse for BindgenInputParams { let key: Ident = input.parse()?; input.parse::()?; let value: LitStr = input.parse()?; - source = Some(source_from_key(&key, value)?); + match key.to_string().as_str() { + "world" | "world_name" => world_name = Some(value.value()), + "path" | "wit" | "wasm" | "inline" => { + source = Some(source_from_key(&key, value)?); + } + _ => return Err(unknown_key_error(&key)), + } } else { let option_path_litstr = input.parse::>()?; if let Some(concrete_path) = option_path_litstr { diff --git a/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs b/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs index a62f09840..1b9b11de1 100644 --- a/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs +++ b/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs @@ -15,12 +15,15 @@ limitations under the License. */ use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use hyperlight_component_util::etypes::{ Defined, ExternDesc, Handleable, ImportExport, TypeBound, Tyvar, Value, }; use hyperlight_component_util::{emit, guest, rtypes, util}; +static TEMP_WASM_COUNTER: AtomicU64 = AtomicU64::new(0); + fn fixture_path(path: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join(path) } @@ -32,8 +35,9 @@ fn encode_wit_fixture_to_wasm(path: &Path) -> PathBuf { .expect("WIT fixture should parse successfully"); let wasm = wit_component::encode(&resolve, package).expect("WIT fixture should encode"); let wasm_path = std::env::temp_dir().join(format!( - "hyperlight-component-util-{}-wit-fixture.wasm", - std::process::id() + "hyperlight-component-util-{}-{}-wit-fixture.wasm", + std::process::id(), + TEMP_WASM_COUNTER.fetch_add(1, Ordering::Relaxed) )); std::fs::write(&wasm_path, wasm).expect("temporary wasm fixture should be written"); wasm_path From 971f23a1cb621e37e3b966b77d8f4557d1df1446 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Fri, 26 Jun 2026 13:31:14 +0200 Subject: [PATCH 3/3] fix: add missing files Signed-off-by: Tomasz Andrzejak --- .../rust_guests/witguest/with-deps/deps/shared/types.wit | 7 +++++++ src/tests/rust_guests/witguest/with-deps/world.wit | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 src/tests/rust_guests/witguest/with-deps/deps/shared/types.wit create mode 100644 src/tests/rust_guests/witguest/with-deps/world.wit diff --git a/src/tests/rust_guests/witguest/with-deps/deps/shared/types.wit b/src/tests/rust_guests/witguest/with-deps/deps/shared/types.wit new file mode 100644 index 000000000..ed7a6a5b7 --- /dev/null +++ b/src/tests/rust_guests/witguest/with-deps/deps/shared/types.wit @@ -0,0 +1,7 @@ +package deps:shared; + +interface types { + record dep-record { + value: string, + } +} diff --git a/src/tests/rust_guests/witguest/with-deps/world.wit b/src/tests/rust_guests/witguest/with-deps/world.wit new file mode 100644 index 000000000..419d19aa8 --- /dev/null +++ b/src/tests/rust_guests/witguest/with-deps/world.wit @@ -0,0 +1,5 @@ +package test:with-deps; + +world with-deps { + import deps:shared/types; +}