hyperlight_component_macro/lib.rs
1/*
2Copyright 2025 The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15 */
16
17//! # Component-model bindgen macros
18//!
19//! These macros make it easy to use Wasm Component Model types
20//! (e.g. those described by WIT) to describe the interface between a
21//! Hyperlight host and guest.
22//!
23//! For both host and guest bindings, bindings generation takes in a
24//! binary-encoded wasm component, which should have roughly the
25//! structure of a binary-encoded WIT (in particular, component
26//! import/export kebab-names should have `wit:package/name` namespace
27//! structure, and the same two-level convention for wrapping a
28//! component type into an actual component should be adhered to). If
29//! you are using WIT as the input, it is easy to build such a file
30//! via `wasm-tools component wit -w -o file.wasm file.wit`.
31//!
32//! Both macros can take the path to such a file as a parameter, or,
33//! if one is not provided, will fall back to using the path in the
34//! environment variable `$WIT_WORLD`. A relative path provided either way
35//! will be resolved relative to `$CARGO_MANIFEST_DIR`.
36//!
37//! ## Debugging
38//!
39//! The generated code can be examined by setting the environment
40//! variable `$HYPERLIGHT_COMPONENT_MACRO_DEBUG=/path/to/file.rs`,
41//! which will result in the generated code being written to that
42//! file, which is then included back into the Rust source.
43//!
44//! The macros also can be configured to output a great deal of debug
45//! information about the internal elaboration and codegen
46//! phases. This is logged via the `log` and `env_logger` crates, so
47//! setting `RUST_LOG=debug` before running the compiler should
48//! suffice to produce this output.
49
50extern crate proc_macro;
51
52use hyperlight_component_util::*;
53use syn::parse::{Parse, ParseStream};
54use syn::{Ident, LitStr, Result, Token};
55
56/// Create host bindings for the wasm component type in the file
57/// passed in (or `$WIT_WORLD`, if nothing is passed in). This will
58/// produce all relevant types and trait implementations for the
59/// component type, as well as functions allowing the component to be
60/// instantiated inside a sandbox.
61///
62/// This includes both a primitive `register_host_functions`, which can
63/// be used to directly register the host functions on any sandbox
64/// (and which can easily be used with Hyperlight-Wasm), as well as an
65/// `instantiate()` method on the component trait that makes
66/// instantiating the sandbox particularly ergonomic in core
67/// Hyperlight.
68#[proc_macro]
69pub fn host_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
70 let _ = env_logger::try_init();
71 let parsed_bindgen_input = syn::parse_macro_input!(input as BindgenInputParams);
72 let path = match parsed_bindgen_input.path {
73 Some(path_env) => path_env.into_os_string(),
74 None => std::env::var_os("WIT_WORLD").unwrap(),
75 };
76 let world_name = parsed_bindgen_input.world_name;
77
78 util::read_wit_type_from_file(path, world_name, |kebab_name, ct| {
79 let decls = emit::run_state(false, false, |s| {
80 rtypes::emit_toplevel(s, &kebab_name, ct);
81 host::emit_toplevel(s, &kebab_name, ct);
82 });
83 util::emit_decls(decls).into()
84 })
85}
86
87/// Create the hyperlight_guest_init() function (which should be
88/// called in hyperlight_main()) for the wasm component type in the
89/// file passed in (or `$WIT_WORLD`, if nothing is passed in). This
90/// function registers Hyperlight functions for component exports
91/// (which are implemented by calling into the trait provided) and
92/// implements the relevant traits for a trivial Host type (by calling
93/// into the Hyperlight host).
94#[proc_macro]
95pub fn guest_bindgen(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
96 let _ = env_logger::try_init();
97 let parsed_bindgen_input = syn::parse_macro_input!(input as BindgenInputParams);
98 let path = match parsed_bindgen_input.path {
99 Some(path_env) => path_env.into_os_string(),
100 None => std::env::var_os("WIT_WORLD").unwrap(),
101 };
102 let world_name = parsed_bindgen_input.world_name;
103
104 util::read_wit_type_from_file(path, world_name, |kebab_name, ct| {
105 let decls = emit::run_state(true, false, |s| {
106 // Emit type/trait definitions for all instances in the world
107 rtypes::emit_toplevel(s, &kebab_name, ct);
108 // Emit the host/guest function registrations
109 guest::emit_toplevel(s, &kebab_name, ct);
110 });
111 // Use util::emit_decls() to choose between emitting the token
112 // stream directly and emitting an include!() pointing at a
113 // temporary file, depending on whether the user has requested
114 // a debug temporary file be created.
115 util::emit_decls(decls).into()
116 })
117}
118
119#[derive(Debug)]
120struct BindgenInputParams {
121 world_name: Option<String>,
122 path: Option<std::path::PathBuf>,
123}
124
125impl Parse for BindgenInputParams {
126 fn parse(input: ParseStream) -> Result<Self> {
127 let mut path = None;
128 let mut world_name = None;
129
130 if input.peek(syn::token::Brace) {
131 let content;
132 syn::braced!(content in input);
133
134 // Parse key-value pairs inside the braces
135 while !content.is_empty() {
136 let key: Ident = content.parse()?;
137 content.parse::<Token![:]>()?;
138
139 match key.to_string().as_str() {
140 "world_name" => {
141 let value: LitStr = content.parse()?;
142 world_name = Some(value.value());
143 }
144 "path" => {
145 let value: LitStr = content.parse()?;
146 path = Some(std::path::PathBuf::from(value.value()));
147 }
148 _ => {
149 return Err(syn::Error::new(
150 key.span(),
151 format!(
152 "unknown parameter '{}'; expected 'path' or 'world_name'",
153 key
154 ),
155 ));
156 }
157 }
158 // Parse optional comma
159 if content.peek(Token![,]) {
160 content.parse::<Token![,]>()?;
161 }
162 }
163 } else {
164 let option_path_litstr = input.parse::<Option<syn::LitStr>>()?;
165 if let Some(concrete_path) = option_path_litstr {
166 path = Some(std::path::PathBuf::from(concrete_path.value()));
167 }
168 }
169 Ok(Self { world_name, path })
170 }
171}