Skip to main content

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}