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}