Skip to main content

hyperlight_guest_macro/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4use proc_macro::TokenStream;
5use proc_macro_crate::{FoundCrate, crate_name};
6use quote::quote;
7use syn::parse::{Error, Parse, ParseStream, Result};
8use syn::spanned::Spanned as _;
9use syn::{ForeignItemFn, ItemFn, LitStr, Pat, parse_macro_input};
10
11/// Represents the optional name argument for the guest_function and host_function macros.
12enum NameArg {
13    None,
14    Name(LitStr),
15}
16
17impl Parse for NameArg {
18    fn parse(input: ParseStream) -> Result<Self> {
19        // accepts either nothing or a single string literal
20        // anything else is an error
21        if input.is_empty() {
22            return Ok(NameArg::None);
23        }
24        let name: LitStr = input.parse()?;
25        if !input.is_empty() {
26            return Err(Error::new(input.span(), "expected a single identifier"));
27        }
28        Ok(NameArg::Name(name))
29    }
30}
31
32/// Attribute macro to mark a function as a guest function.
33/// This will register the function so that it can be called by the host.
34///
35/// If a name is provided as an argument, that name will be used to register the function.
36/// Otherwise, the function's identifier will be used.
37///
38/// The function arguments must be supported parameter types, and the return type must be
39/// a supported return type or a `Result<T, HyperlightGuestError>` with T being a supported
40/// return type.
41///
42/// # Note
43/// The function will be registered with the host at program initialization regardless of
44/// the visibility modifier used (e.g., `pub`, `pub(crate)`, etc.).
45/// This means that a private functions can be called by the host from beyond its normal
46/// visibility scope.
47///
48/// # Example
49/// ```ignore
50/// use hyperlight_guest_bin::guest_function;
51/// #[guest_function]
52/// fn my_guest_function(arg1: i32, arg2: String) -> i32 {
53///     arg1 + arg2.len() as i32
54/// }
55/// ```
56///
57/// or with a custom name:
58/// ```ignore
59/// use hyperlight_guest_bin::guest_function;
60/// #[guest_function("custom_name")]
61/// fn my_guest_function(arg1: i32, arg2: String) -> i32 {
62///     arg1 + arg2.len() as i32
63/// }
64/// ```
65///
66/// or with a Result return type:
67/// ```ignore
68/// use hyperlight_guest_bin::guest_function;
69/// use hyperlight_guest::bail;
70/// #[guest_function]
71/// fn my_guest_function(arg1: i32, arg2: String) -> Result<i32, HyperlightGuestError> {
72///     bail!("An error occurred");
73/// }
74/// ```
75#[proc_macro_attribute]
76pub fn guest_function(attr: TokenStream, item: TokenStream) -> TokenStream {
77    // Obtain the crate name for hyperlight-guest-bin
78    let crate_name =
79        crate_name("hyperlight-guest-bin").expect("hyperlight-guest-bin must be a dependency");
80    let crate_name = match crate_name {
81        FoundCrate::Itself => quote! {crate},
82        FoundCrate::Name(name) => {
83            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
84            quote! {::#ident}
85        }
86    };
87
88    // Parse the function definition that we will be working with, and
89    // early return if parsing as `ItemFn` fails.
90    let fn_declaration = parse_macro_input!(item as ItemFn);
91
92    // Obtain the name of the function being decorated.
93    let ident = fn_declaration.sig.ident.clone();
94
95    // Determine the name used to register the function, either
96    // the provided name or the function's identifier.
97    let exported_name = match parse_macro_input!(attr as NameArg) {
98        NameArg::None => quote! { stringify!(#ident) },
99        NameArg::Name(name) => quote! { #name },
100    };
101
102    // Small sanity checks to improve error messages.
103    // These checks are not strictly necessary, as the generated code
104    // would fail to compile anyway (due to the trait bounds of `register_fn`),
105    // but they provide better feedback to the user of the macro.
106
107    // Check that there are no receiver arguments (i.e., `self`, `&self`, `Box<Self>`, etc).
108    if let Some(syn::FnArg::Receiver(arg)) = fn_declaration.sig.inputs.first() {
109        return Error::new(
110            arg.span(),
111            "Receiver (self) argument is not allowed in guest functions",
112        )
113        .to_compile_error()
114        .into();
115    }
116
117    // Check that the function is not async.
118    if fn_declaration.sig.asyncness.is_some() {
119        return Error::new(
120            fn_declaration.sig.asyncness.span(),
121            "Async functions are not allowed in guest functions",
122        )
123        .to_compile_error()
124        .into();
125    }
126
127    // The generated code will replace the decorated code, so we need to
128    // include the original function declaration in the output.
129    let output = quote! {
130        #fn_declaration
131
132        const _: () = {
133            // Add the function registration in the GUEST_FUNCTION_INIT distributed slice
134            // so that it can be registered at program initialization
135            #[#crate_name::__private::linkme::distributed_slice(#crate_name::__private::GUEST_FUNCTION_INIT)]
136            #[linkme(crate = #crate_name::__private::linkme)]
137            static REGISTRATION: fn() = || {
138                #crate_name::guest_function::register::register_fn(#exported_name, #ident);
139            };
140        };
141    };
142
143    output.into()
144}
145
146/// Attribute macro to mark a function as the main entry point for the guest.
147/// This will generate a function that is called by the host at program initialization.
148///
149/// # Example
150/// ```ignore
151/// use hyperlight_guest_bin::main;
152/// #[main]
153/// fn main() {
154///     // do some initialization work here, e.g., initialize global state, etc.
155/// }
156/// ```
157#[proc_macro_attribute]
158pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
159    // Parse the function definition that we will be working with, and
160    // early return if parsing as `ItemFn` fails.
161    let fn_declaration = parse_macro_input!(item as ItemFn);
162
163    // Obtain the name of the function being decorated.
164    let ident = fn_declaration.sig.ident.clone();
165
166    // The generated code will replace the decorated code, so we need to
167    // include the original function declaration in the output.
168    let output = quote! {
169        #fn_declaration
170
171        const _: () = {
172            mod wrapper {
173                #[unsafe(no_mangle)]
174                pub extern "C" fn hyperlight_main() {
175                    super::#ident()
176                }
177            }
178        };
179    };
180
181    output.into()
182}
183
184/// Attribute macro to mark a function as the dispatch function for the guest.
185/// This is the function that will be called by the host when a function call is made
186/// to a function that is not registered with the host.
187///
188/// # Example
189/// ```ignore
190/// use hyperlight_guest_bin::dispatch;
191/// use hyperlight_guest::error::Result;
192/// use hyperlight_guest::bail;
193/// use hyperlight_common::flatbuffer_wrappers::function_call::FunctionCall;
194/// use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result;
195/// #[dispatch]
196/// fn dispatch(fc: FunctionCall) -> Result<Vec<u8>> {
197///     let name = &fc.function_name;
198///     if name == "greet" {
199///         return Ok(get_flatbuffer_result("Hello, world!"));
200///     }
201///     bail!("Unknown function: {name}");
202/// }
203/// ```
204#[proc_macro_attribute]
205pub fn dispatch(_attr: TokenStream, item: TokenStream) -> TokenStream {
206    // Obtain the crate name for hyperlight-guest-bin
207    let crate_name =
208        crate_name("hyperlight-guest-bin").expect("hyperlight-guest-bin must be a dependency");
209    let crate_name = match crate_name {
210        FoundCrate::Itself => quote! {crate},
211        FoundCrate::Name(name) => {
212            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
213            quote! {::#ident}
214        }
215    };
216
217    // Parse the function definition that we will be working with, and
218    // early return if parsing as `ItemFn` fails.
219    let fn_declaration = parse_macro_input!(item as ItemFn);
220
221    // Obtain the name of the function being decorated.
222    let ident = fn_declaration.sig.ident.clone();
223
224    // The generated code will replace the decorated code, so we need to
225    // include the original function declaration in the output.
226    let output = quote! {
227        #fn_declaration
228
229        const _: () = {
230            mod wrapper {
231                use #crate_name::__private::{FunctionCall, HyperlightGuestError, Vec};
232                #[unsafe(no_mangle)]
233                pub fn guest_dispatch_function(function_call: FunctionCall) -> ::core::result::Result<Vec<u8>, HyperlightGuestError> {
234                    super::#ident(function_call)
235                }
236            }
237        };
238    };
239
240    output.into()
241}
242
243/// Attribute macro to mark a function as a host function.
244/// This will generate a function that calls the host function with the same name.
245///
246/// If a name is provided as an argument, that name will be used to call the host function.
247/// Otherwise, the function's identifier will be used.
248///
249/// The function arguments must be supported parameter types, and the return type must be
250/// a supported return type or a `Result<T, HyperlightGuestError>` with T being a supported
251/// return type.
252///
253/// # Panic
254/// If the return type is not a Result, the generated function will panic if the host function
255/// returns an error.
256///
257/// # Example
258/// ```ignore
259/// use hyperlight_guest_bin::host_function;
260/// #[host_function]
261/// fn my_host_function(arg1: i32, arg2: String) -> i32;
262/// ```
263///
264/// or with a custom name:
265/// ```ignore
266/// use hyperlight_guest_bin::host_function;
267/// #[host_function("custom_name")]
268/// fn my_host_function(arg1: i32, arg2: String) -> i32;
269/// ```
270///
271/// or with a Result return type:
272/// ```ignore
273/// use hyperlight_guest_bin::host_function;
274/// use hyperlight_guest::error::HyperlightGuestError;
275/// #[host_function]
276/// fn my_host_function(arg1: i32, arg2: String) -> Result<i32, HyperlightGuestError>;
277/// ```
278#[proc_macro_attribute]
279pub fn host_function(attr: TokenStream, item: TokenStream) -> TokenStream {
280    // Obtain the crate name for hyperlight-guest-bin
281    let crate_name =
282        crate_name("hyperlight-guest-bin").expect("hyperlight-guest-bin must be a dependency");
283    let crate_name = match crate_name {
284        FoundCrate::Itself => quote! {crate},
285        FoundCrate::Name(name) => {
286            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
287            quote! {::#ident}
288        }
289    };
290
291    // Parse the function declaration that we will be working with, and
292    // early return if parsing as `ForeignItemFn` fails.
293    // A function declaration without a body is a foreign item function, as that's what
294    // you would use when declaring an FFI function.
295    let fn_declaration = parse_macro_input!(item as ForeignItemFn);
296
297    // Destructure the foreign item function to get its components.
298    let ForeignItemFn {
299        attrs,
300        vis,
301        sig,
302        semi_token: _,
303        modifiers: _,
304    } = fn_declaration;
305
306    // Obtain the name of the function being decorated.
307    let ident = sig.ident.clone();
308
309    // Determine the name used to call the host function, either
310    // the provided name or the function's identifier.
311    let exported_name = match parse_macro_input!(attr as NameArg) {
312        NameArg::None => quote! { stringify!(#ident) },
313        NameArg::Name(name) => quote! { #name },
314    };
315
316    // Build the list of argument identifiers to pass to the call_host function.
317    // While doing that, also do some sanity checks to improve error messages.
318    // These checks are not strictly necessary, as the generated code would fail
319    // to compile anyway due to either:
320    // * the trait bounds of `call_host`
321    // * the generated code having invalid syntax
322    // but they provide better feedback to the user of the macro, especially in
323    // the case of invalid syntax.
324    let mut args = vec![];
325    for arg in sig.inputs.iter() {
326        match arg {
327            // Reject receiver arguments (i.e., `self`, `&self`, `Box<Self>`, etc).
328            syn::FnArg::Receiver(_) => {
329                return Error::new(
330                    arg.span(),
331                    "Receiver (self) argument is not allowed in guest functions",
332                )
333                .to_compile_error()
334                .into();
335            }
336            syn::FnArg::Typed(arg) => {
337                // A typed argument: `name: Type`
338                // Technically, the `name` part can be any pattern, e.g., destructuring patterns
339                // like `(a, b): (i32, u64)`, but we only allow simple identifiers here
340                // to keep things simple.
341
342                // Reject anything that is not a simple identifier.
343                let Pat::Ident(pat) = *arg.pat.clone() else {
344                    return Error::new(
345                        arg.span(),
346                        "Only named arguments are allowed in host functions",
347                    )
348                    .to_compile_error()
349                    .into();
350                };
351
352                // Reject any argument with attributes, e.g., `#[cfg(feature = "gdb")] name: Type`
353                if !pat.attrs.is_empty() {
354                    return Error::new(
355                        arg.span(),
356                        "Attributes are not allowed on host function arguments",
357                    )
358                    .to_compile_error()
359                    .into();
360                }
361
362                // Reject any argument passed by reference
363                if pat.by_ref.is_some() {
364                    return Error::new(
365                        arg.span(),
366                        "By-ref arguments are not allowed in host functions",
367                    )
368                    .to_compile_error()
369                    .into();
370                }
371
372                // Reject any mutable argument, e.g., `mut name: Type`
373                if pat.mutability.is_some() {
374                    return Error::new(
375                        arg.span(),
376                        "Mutable arguments are not allowed in host functions",
377                    )
378                    .to_compile_error()
379                    .into();
380                }
381
382                // Reject any sub-patterns
383                if pat.subpat.is_some() {
384                    return Error::new(
385                        arg.span(),
386                        "Sub-patterns are not allowed in host functions",
387                    )
388                    .to_compile_error()
389                    .into();
390                }
391
392                let ident = pat.ident.clone();
393
394                // All checks passed, add the identifier to the argument list.
395                args.push(quote! { #ident });
396            }
397        }
398    }
399
400    // Determine the return type of the function.
401    // If the return type is not specified, it is `()`.
402    let ret: proc_macro2::TokenStream = match &sig.output {
403        syn::ReturnType::Default => quote! { quote! { () } },
404        syn::ReturnType::Type(_, ty) => {
405            quote! { #ty }
406        }
407    };
408
409    // Take the parts of the function declaration and generate a function definition
410    // matching the provided declaration, but with a body that calls the host function.
411    let output = quote! {
412        #(#attrs)* #vis #sig {
413            use #crate_name::__private::FromResult;
414            use #crate_name::host_comm::call_host;
415            <#ret as FromResult>::from_result(call_host(#exported_name, (#(#args,)*)))
416        }
417    };
418
419    output.into()
420}