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