diff --git a/Justfile b/Justfile index 82f137466..8b218b035 100644 --- a/Justfile +++ b/Justfile @@ -238,10 +238,13 @@ 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="": +test-integration target=default-target features="": (witguest-wit) @# 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 + {{ 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 {{ cargo-cmd }} test -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*' diff --git a/src/hyperlight_component_util/src/emit.rs b/src/hyperlight_component_util/src/emit.rs index d6c67cdf7..fe5c97034 100644 --- a/src/hyperlight_component_util/src/emit.rs +++ b/src/hyperlight_component_util/src/emit.rs @@ -763,6 +763,21 @@ pub fn kebab_to_imports_name(trait_name: &str) -> Ident { pub fn kebab_to_exports_name(trait_name: &str) -> Ident { format_ident!("{}Exports", kebab_to_type(trait_name)) } +/// Convert a kebab name to a SCREAMING_SNAKE_CASE identifier suitable +/// for use as a constant in a `wasmtime::component::flags!` invocation. +pub fn kebab_to_flags_const(n: &str) -> Ident { + let s: String = n + .chars() + .map(|c| { + if c == '-' { + '_' + } else { + c.to_ascii_uppercase() + } + }) + .collect(); + format_ident!("{}", s) +} /// The kinds of names that a function associated with a resource in /// WIT can have diff --git a/src/hyperlight_component_util/src/hl.rs b/src/hyperlight_component_util/src/hl.rs index 5b7211cf3..a3ffe6f6d 100644 --- a/src/hyperlight_component_util/src/hl.rs +++ b/src/hyperlight_component_util/src/hl.rs @@ -18,7 +18,7 @@ use itertools::Itertools; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; -use crate::emit::{ResolvedBoundVar, State, kebab_to_cons, kebab_to_var}; +use crate::emit::{ResolvedBoundVar, State, kebab_to_cons, kebab_to_flags_const, kebab_to_var}; use crate::etypes::{self, Defined, Handleable, Tyvar, Value}; use crate::rtypes; @@ -91,16 +91,37 @@ pub fn emit_hl_unmarshal_toplevel_value( } Value::Flags(ns) => { let bytes = usize::div_ceil(ns.len(), 8); + let result_var = format_ident!("{}_flags", id); let fields = ns.iter().enumerate().map(|(i, n)| { let byte_offset = i / 8; let bit_offset = i % 8; - let fieldid = kebab_to_var(n.name); - quote! { - #fieldid: (#id[#byte_offset] >> #bit_offset) & 0x1 == 1, + let is_set = quote! { (#id[#byte_offset] >> #bit_offset) & 0x1 == 1 }; + if s.is_wasmtime_guest { + let const_name = kebab_to_flags_const(n.name); + quote! { + if #is_set { + #result_var |= #tname::#const_name; + } + } + } else { + let fieldid = kebab_to_var(n.name); + quote! { + #fieldid: #is_set, + } } }); - quote! { - (#tname { #(#fields)* }, #bytes) + if s.is_wasmtime_guest { + quote! { + { + let mut #result_var = #tname::empty(); + #(#fields)* + (#result_var, #bytes) + } + } + } else { + quote! { + (#tname { #(#fields)* }, #bytes) + } } } Value::Variant(vcs) => { @@ -434,9 +455,15 @@ pub fn emit_hl_marshal_toplevel_value( .map(|(i, n)| { let byte_offset = i / 8; let bit_offset = i % 8; - let fieldid = kebab_to_var(n.name); + let is_set = if s.is_wasmtime_guest { + let const_name = kebab_to_flags_const(n.name); + quote! { #id.contains(#tname::#const_name) } + } else { + let fieldid = kebab_to_var(n.name); + quote! { #id.#fieldid } + }; quote! { - bytes[#byte_offset] |= (if #id.#fieldid { 1 } else { 0 }) << #bit_offset; + bytes[#byte_offset] |= (if #is_set { 1 } else { 0 }) << #bit_offset; } }) .collect::>(); diff --git a/src/hyperlight_component_util/src/rtypes.rs b/src/hyperlight_component_util/src/rtypes.rs index d6c4a8b38..647b11c36 100644 --- a/src/hyperlight_component_util/src/rtypes.rs +++ b/src/hyperlight_component_util/src/rtypes.rs @@ -24,9 +24,9 @@ use quote::{format_ident, quote}; use syn::Ident; use crate::emit::{ - FnName, ResourceItemName, State, WitName, kebab_to_cons, kebab_to_exports_name, kebab_to_fn, - kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, kebab_to_type, kebab_to_var, - split_wit_name, + FnName, ResourceItemName, State, WitName, kebab_to_cons, kebab_to_exports_name, + kebab_to_flags_const, kebab_to_fn, kebab_to_getter, kebab_to_imports_name, kebab_to_namespace, + kebab_to_type, kebab_to_var, split_wit_name, }; use crate::etypes::{ self, Component, Defined, ExternDecl, ExternDesc, Func, Handleable, ImportExport, Instance, @@ -409,21 +409,41 @@ fn emit_value_toplevel(s: &mut State, v: Option, id: Ident, vt: &Value) -> } } Value::Flags(ns) => { - let (vs, toks) = gather_needed_vars(s, v, |_| { - let ns = ns + if s.is_wasmtime_guest { + let flags = ns .iter() .map(|n| { let orig_name = n.name; - let id = kebab_to_var(orig_name); - quote! { pub #id: bool } + let const_name = kebab_to_flags_const(orig_name); + quote! { + #[component(name = #orig_name)] + const #const_name; + } }) .collect::>(); - quote! { #(#ns),* } - }); - let vs = emit_type_defn_var_list(s, vs); - quote! { - #[derive(Debug, Clone, PartialEq)] - pub struct #id #vs { #toks } + quote! { + ::wasmtime::component::flags! { + #id { + #(#flags)* + } + } + } + } else { + let (vs, toks) = gather_needed_vars(s, v, |_| { + let ns = ns + .iter() + .map(|n| { + let id = kebab_to_var(n.name); + quote! { pub #id: bool } + }) + .collect::>(); + quote! { #(#ns),* } + }); + let vs = emit_type_defn_var_list(s, vs); + quote! { + #[derive(Debug, Clone, PartialEq)] + pub struct #id #vs { #toks } + } } } Value::Variant(vcs) => { diff --git a/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs b/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs new file mode 100644 index 000000000..a9754c8ef --- /dev/null +++ b/src/hyperlight_component_util/tests/wasmtime_guest_codegen.rs @@ -0,0 +1,47 @@ +/* +Copyright 2026 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. +*/ + +use hyperlight_component_util::{emit, guest, rtypes, util}; + +#[test] +fn wasmtime_guest_codegen_emits_wasmtime_flags_macro() { + let generated = util::read_wit_type_from_file( + "../tests/rust_guests/witguest/interface.wasm", + None, + |kebab_name, ct| { + // Mirrors the hyperlight-wasm guest-bindgen expansion path: + // https://github.com/hyperlight-dev/hyperlight-wasm/blob/81e72f5920ebc23584097abfe24d05a40bf084cc/src/hyperlight_wasm_macro/src/lib.rs#L37-L38 + emit::run_state(true, true, |s| { + rtypes::emit_toplevel(s, &kebab_name, ct); + guest::emit_toplevel(s, &kebab_name, ct); + }) + }, + ); + let generated: syn::File = syn::parse2(generated).expect("generated Rust should parse"); + let generated = prettyplease::unparse(&generated); + + assert!(generated.contains("::wasmtime::component::flags! {")); + assert!(generated.contains("Smallflags {")); + assert!(generated.contains("\"flag-a\"")); + assert!(generated.contains("const FLAG_A;")); + assert!(generated.contains("\"flag-b\"")); + assert!(generated.contains("const FLAG_B;")); + assert!(generated.contains("\"flag-c\"")); + assert!(generated.contains("const FLAG_C;")); + assert!(!generated.contains("pub flag_a: bool")); + assert!(!generated.contains("pub flag_b: bool")); + assert!(!generated.contains("pub flag_c: bool")); +}