Skip to main content

nub_host_guest_macro/
lib.rs

1//! Proc macros for the nub guest runtime.
2//!
3//! Forked from `hyperlight-guest-macro 0.15.0` (Apache-2.0), stripped
4//! of name-based + parameter-polymorphic registration. The single
5//! attribute `#[guest_function(fn_id = N)]` registers a guest
6//! function under an integer `fn_id` chosen at compile time. The
7//! dispatcher in `nub-arch-guestbin` matches on `fn_id` and calls
8//! into the registered function pointer.
9//!
10//! The function being annotated must have signature
11//! `fn(&[u8]) -> Vec<u8>` (the request payload bytes → response
12//! payload bytes). Typed encode/decode is the caller's job — the
13//! macro stays out of the codec layer.
14//!
15//! `#[host_function]` is forked too: the wrapper around
16//! `nub_arch_guestbin::host_comm::call_host_raw(fn_id, &bytes)` for
17//! guest→host RPC. The `#[main]` and `#[dispatch]` upstream
18//! attributes are dropped; we use the weak-linkage default for
19//! `hyperlight_main` and hand-wire the dispatcher.
20
21use proc_macro::TokenStream;
22use proc_macro_crate::{FoundCrate, crate_name};
23use proc_macro2::Span;
24use quote::quote;
25use syn::parse::{Parse, ParseStream};
26use syn::spanned::Spanned;
27use syn::{Error, Expr, ItemFn, Token, parse_macro_input};
28
29/// Parses `fn_id = <expr>` from the attribute args. The expression
30/// is forwarded verbatim into the emitted static initializer, so it
31/// can be a `u32` literal, a path to a `const FN_ID_*: u32` (the
32/// usual case), or any other const-evaluable u32 expression.
33struct FnIdArg(Expr);
34
35impl Parse for FnIdArg {
36    fn parse(input: ParseStream) -> syn::Result<Self> {
37        let ident: syn::Ident = input.parse()?;
38        if ident != "fn_id" {
39            return Err(Error::new(
40                ident.span(),
41                "expected `fn_id = <const u32 expression>`",
42            ));
43        }
44        let _eq: Token![=] = input.parse()?;
45        let expr: Expr = input.parse()?;
46        Ok(FnIdArg(expr))
47    }
48}
49
50fn resolve_crate(name: &str) -> proc_macro2::TokenStream {
51    match crate_name(name).unwrap_or_else(|_| panic!("`{name}` must be a dependency")) {
52        FoundCrate::Itself => quote! { crate },
53        FoundCrate::Name(found) => {
54            let ident = syn::Ident::new(&found, Span::call_site());
55            quote! { ::#ident }
56        }
57    }
58}
59
60/// Register a guest function under a compile-time `fn_id`.
61///
62/// ```ignore
63/// use nub_host_guest_macro::guest_function;
64///
65/// #[guest_function(fn_id = 1)]
66/// pub fn nub_invoke(input: &[u8]) -> Vec<u8> {
67///     // ...
68/// }
69/// ```
70///
71/// Expands to the original function plus a `linkme` distributed-slice
72/// entry that the guestbin's `dispatch` function iterates at call
73/// time. The function signature is fixed: `fn(&[u8]) -> Vec<u8>`.
74#[proc_macro_attribute]
75pub fn guest_function(attr: TokenStream, item: TokenStream) -> TokenStream {
76    let crate_name = resolve_crate("hyperlight-guest-bin");
77    let FnIdArg(fn_id_expr) = parse_macro_input!(attr as FnIdArg);
78    let fn_declaration = parse_macro_input!(item as ItemFn);
79
80    let ident = fn_declaration.sig.ident.clone();
81
82    // Sanity: reject receiver args and async functions.
83    if let Some(syn::FnArg::Receiver(arg)) = fn_declaration.sig.inputs.first() {
84        return Error::new(arg.span(), "receiver (self) argument not allowed")
85            .to_compile_error()
86            .into();
87    }
88    if fn_declaration.sig.asyncness.is_some() {
89        return Error::new(
90            fn_declaration.sig.asyncness.span(),
91            "async guest functions not allowed",
92        )
93        .to_compile_error()
94        .into();
95    }
96
97    let registration_ident =
98        syn::Ident::new(&format!("__NUB_GUEST_FN_{ident}_ENTRY"), Span::call_site());
99
100    quote! {
101        #fn_declaration
102
103        #[#crate_name::__private::linkme::distributed_slice(
104            #crate_name::__private::GUEST_FUNCTION_TABLE
105        )]
106        #[linkme(crate = #crate_name::__private::linkme)]
107        static #registration_ident:
108            #crate_name::guest_function::register::GuestFnEntry =
109            #crate_name::guest_function::register::GuestFnEntry {
110                fn_id: (#fn_id_expr),
111                dispatcher: #ident,
112            };
113    }
114    .into()
115}
116
117/// Generate a host-function wrapper that issues a guest→host RPC.
118///
119/// ```ignore
120/// use nub_host_guest_macro::host_function;
121///
122/// #[host_function(fn_id = 10)]
123/// pub fn read_block(block_id: &[u8]) -> Vec<u8>;
124/// ```
125///
126/// Expands the foreign-item-function declaration into a real
127/// function whose body calls
128/// `nub_arch_guestbin::host_comm::call_host_raw(fn_id, payload)`.
129/// Like `#[guest_function]`, the signature must be
130/// `fn(&[u8]) -> Vec<u8>` — typed encode/decode is the caller's job.
131#[proc_macro_attribute]
132pub fn host_function(attr: TokenStream, item: TokenStream) -> TokenStream {
133    let crate_name = resolve_crate("hyperlight-guest-bin");
134    let FnIdArg(fn_id_expr) = parse_macro_input!(attr as FnIdArg);
135    let fn_declaration = parse_macro_input!(item as syn::ForeignItemFn);
136
137    let syn::ForeignItemFn {
138        attrs, vis, sig, ..
139    } = fn_declaration;
140
141    // Validate signature: one `&[u8]` arg, `Vec<u8>` return.
142    if sig.inputs.len() != 1 {
143        return Error::new(sig.inputs.span(), "expected exactly one `&[u8]` parameter")
144            .to_compile_error()
145            .into();
146    }
147
148    // The single parameter's pattern (e.g. `payload: &[u8]`) — we
149    // need its identifier to forward into `call_host_raw`.
150    let arg_ident = match sig.inputs.first().unwrap() {
151        syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
152            syn::Pat::Ident(pat_ident) => pat_ident.ident.clone(),
153            _ => {
154                return Error::new(
155                    pat_type.pat.span(),
156                    "expected a plain identifier pattern (e.g. `payload: &[u8]`)",
157                )
158                .to_compile_error()
159                .into();
160            }
161        },
162        syn::FnArg::Receiver(arg) => {
163            return Error::new(arg.span(), "receiver (self) argument not allowed")
164                .to_compile_error()
165                .into();
166        }
167    };
168
169    quote! {
170        #(#attrs)*
171        #vis #sig {
172            #crate_name::host_comm::call_host_raw((#fn_id_expr), #arg_ident)
173                .expect("host function call failed")
174        }
175    }
176    .into()
177}