Skip to main content

hyperlight_component_util/
util.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4//! General utilities for bindgen macros
5use crate::etypes;
6
7/// Input accepted by component bindgen.
8#[derive(Debug)]
9pub enum WitSource {
10    Wasm(std::path::PathBuf),
11    Wat(std::path::PathBuf),
12    Wit(std::path::PathBuf),
13    Inline(String),
14}
15
16impl WitSource {
17    fn encode(self) -> Vec<u8> {
18        match self {
19            Self::Wasm(path) => {
20                let path = manifest_path(&path);
21                let bytes = std::fs::read(&path).unwrap_or_else(|err| {
22                    panic!(
23                        "failed to read wasm-encoded WIT input '{}': {err}",
24                        path.display()
25                    )
26                });
27                if !wasmparser::Parser::is_component(&bytes) {
28                    panic!(
29                        "wasm-encoded WIT input '{}' is not a wasm component",
30                        path.display()
31                    );
32                }
33                bytes
34            }
35            Self::Wat(path) => {
36                let path = manifest_path(&path);
37                let bytes = wat::parse_file(&path).unwrap_or_else(|err| {
38                    panic!("failed to read wat input '{}': {err:#}", path.display());
39                });
40                if !wasmparser::Parser::is_component(&bytes) {
41                    panic!("wat input '{}' is not a wasm component", path.display());
42                }
43                bytes
44            }
45            Self::Wit(path) => {
46                let path = manifest_path(&path);
47                let mut resolve = wit_parser::Resolve::default();
48                let (package, _) = resolve.push_path(&path).unwrap_or_else(|err| {
49                    panic!("failed to parse WIT input '{}': {err:#}", path.display())
50                });
51
52                wit_component::encode(&resolve, package).unwrap_or_else(|err| {
53                    panic!(
54                        "failed to encode WIT input '{}' as a wasm component type: {err:#}",
55                        path.display()
56                    )
57                })
58            }
59            Self::Inline(contents) => {
60                match wat::Detect::from_bytes(&contents) {
61                    wat::Detect::WasmBinary => {
62                        panic!("inline component type looks like a binary!")
63                    }
64                    wat::Detect::WasmText => {
65                        let bytes = wat::parse_str(&contents).unwrap_or_else(|err| {
66                            panic!("failed to read inline wat input: {err:#}")
67                        });
68                        if !wasmparser::Parser::is_component(&bytes) {
69                            panic!("inline wat input is not a wasm component");
70                        }
71                        bytes
72                    }
73                    wat::Detect::Unknown => {
74                        // It's probably WIT
75                        let mut resolve = wit_parser::Resolve::default();
76                        let package =
77                            resolve
78                                .push_str("inline.wit", &contents)
79                                .unwrap_or_else(|err| {
80                                    panic!("failed to parse inline WIT input: {err:#}")
81                                });
82                        wit_component::encode(&resolve, package).unwrap_or_else(|err| {
83                            panic!("failed to encode inline WIT input as a wasm component type: {err:#}")
84                        })
85                    }
86                }
87            }
88        }
89    }
90}
91
92/// Resolve `path` against the crate being built, or the working
93/// directory outside a cargo build.
94fn manifest_path(path: &std::path::Path) -> std::path::PathBuf {
95    match std::env::var_os("CARGO_MANIFEST_DIR") {
96        Some(dir) => std::path::Path::new(&dir).join(path),
97        None => path.to_path_buf(),
98    }
99}
100
101/// Read and parse a WIT type from a supported bindgen input.
102pub fn read_wit_type<R, F: FnMut(String, &etypes::Component) -> R>(
103    source: WitSource,
104    world_name: Option<String>,
105    mut cb: F,
106) -> R {
107    let bytes = source.encode();
108    let i = wasmparser::Parser::new(0).parse_all(&bytes);
109    let ct = crate::component::read_component_single_exported_type(i, world_name);
110
111    // because of the two-level encapsulation scheme, we need to look
112    // for the single export of the component type that we just read
113    if !ct.uvars.is_empty()
114        || !ct.imports.is_empty()
115        || !ct.instance.evars.is_empty()
116        || ct.instance.unqualified.exports.len() != 1
117    {
118        panic!("malformed component type container for wit type");
119    };
120    let export = &ct.instance.unqualified.exports[0];
121    use etypes::ExternDesc;
122    let ExternDesc::Component(ct) = &export.desc else {
123        panic!("malformed component type container: does not contain component type");
124    };
125    tracing::debug!("hcm: considering component type {:?}", ct);
126    cb(export.kebab_name.to_string(), ct)
127}
128
129/// Read and parse a wasm-encoded WIT file, relative to the cargo manifest
130/// directory.
131pub fn read_wit_type_from_file<R, F: FnMut(String, &etypes::Component) -> R>(
132    filename: impl AsRef<std::ffi::OsStr>,
133    world_name: Option<String>,
134    cb: F,
135) -> R {
136    let src = WitSource::Wasm(std::path::PathBuf::from(filename.as_ref()));
137    read_wit_type(src, world_name, cb)
138}
139
140/// Deal with `$HYPERLIGHT_COMPONENT_MACRO_DEBUG`: if it is present,
141/// save the given token stream (representing the result of
142/// macroexpansion) to the debug file and then return the token stream
143pub fn emit_decls(decls: proc_macro2::TokenStream, for_kebab: &str) -> proc_macro2::TokenStream {
144    if let Ok(dbg_out) = std::env::var("HYPERLIGHT_COMPONENT_MACRO_DEBUG") {
145        let fs_safe = for_kebab.replace("/", "_");
146        #[cfg(windows)]
147        let fs_safe = fs_safe.replace(":", "+");
148        let dbg_out = dbg_out.replace("#", &fs_safe);
149        if let Ok(file) = syn::parse2(decls.clone()) {
150            std::fs::write(&dbg_out, prettyplease::unparse(&file)).unwrap();
151        } else {
152            let decls = format!("{}", decls);
153            std::fs::write(&dbg_out, &decls).unwrap();
154        }
155        quote::quote! { include!(#dbg_out); }
156    } else {
157        decls
158    }
159}