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 the main entry point for the guest.
160/// This will generate a function that is called by the host at program initialization.
161///
162/// # Example
163/// ```ignore
164/// use hyperlight_guest_bin::main;
165/// #[main]
166/// fn main() {
167/// // do some initialization work here, e.g., initialize global state, etc.
168/// }
169/// ```
170#[proc_macro_attribute]
171pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
172 // Parse the function definition that we will be working with, and
173 // early return if parsing as `ItemFn` fails.
174 let fn_declaration = parse_macro_input!(item as ItemFn);
175
176 // Obtain the name of the function being decorated.
177 let ident = fn_declaration.sig.ident.clone();
178
179 // The generated code will replace the decorated code, so we need to
180 // include the original function declaration in the output.
181 let output = quote! {
182 #fn_declaration
183
184 const _: () = {
185 mod wrapper {
186 #[unsafe(no_mangle)]
187 pub extern "C" fn hyperlight_main() {
188 super::#ident()
189 }
190 }
191 };
192 };
193
194 output.into()
195}
196
197/// Attribute macro to mark a function as the dispatch function for the guest.
198/// This is the function that will be called by the host when a function call is made
199/// to a function that is not registered with the host.
200///
201/// # Example
202/// ```ignore
203/// use hyperlight_guest_bin::dispatch;
204/// use hyperlight_guest::error::Result;
205/// use hyperlight_guest::bail;
206/// use hyperlight_common::flatbuffer_wrappers::function_call::FunctionCall;
207/// use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result;
208/// #[dispatch]
209/// fn dispatch(fc: FunctionCall) -> Result<Vec<u8>> {
210/// let name = &fc.function_name;
211/// if name == "greet" {
212/// return Ok(get_flatbuffer_result("Hello, world!"));
213/// }
214/// bail!("Unknown function: {name}");
215/// }
216/// ```
217#[proc_macro_attribute]
218pub fn dispatch(_attr: TokenStream, item: TokenStream) -> TokenStream {
219 // Obtain the crate name for hyperlight-guest-bin
220 let crate_name =
221 crate_name("hyperlight-guest-bin").expect("hyperlight-guest-bin must be a dependency");
222 let crate_name = match crate_name {
223 FoundCrate::Itself => quote! {crate},
224 FoundCrate::Name(name) => {
225 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
226 quote! {::#ident}
227 }
228 };
229
230 // Parse the function definition that we will be working with, and
231 // early return if parsing as `ItemFn` fails.
232 let fn_declaration = parse_macro_input!(item as ItemFn);
233
234 // Obtain the name of the function being decorated.
235 let ident = fn_declaration.sig.ident.clone();
236
237 // The generated code will replace the decorated code, so we need to
238 // include the original function declaration in the output.
239 let output = quote! {
240 #fn_declaration
241
242 const _: () = {
243 mod wrapper {
244 use #crate_name::__private::{FunctionCall, HyperlightGuestError, Vec};
245 #[unsafe(no_mangle)]
246 pub fn guest_dispatch_function(function_call: FunctionCall) -> ::core::result::Result<Vec<u8>, HyperlightGuestError> {
247 super::#ident(function_call)
248 }
249 }
250 };
251 };
252
253 output.into()
254}
255
256/// Attribute macro to mark a function as a host function.
257/// This will generate a function that calls the host function with the same name.
258///
259/// If a name is provided as an argument, that name will be used to call the host function.
260/// Otherwise, the function's identifier will be used.
261///
262/// The function arguments must be supported parameter types, and the return type must be
263/// a supported return type or a `Result<T, HyperlightGuestError>` with T being a supported
264/// return type.
265///
266/// # Panic
267/// If the return type is not a Result, the generated function will panic if the host function
268/// returns an error.
269///
270/// # Example
271/// ```ignore
272/// use hyperlight_guest_bin::host_function;
273/// #[host_function]
274/// fn my_host_function(arg1: i32, arg2: String) -> i32;
275/// ```
276///
277/// or with a custom name:
278/// ```ignore
279/// use hyperlight_guest_bin::host_function;
280/// #[host_function("custom_name")]
281/// fn my_host_function(arg1: i32, arg2: String) -> i32;
282/// ```
283///
284/// or with a Result return type:
285/// ```ignore
286/// use hyperlight_guest_bin::host_function;
287/// use hyperlight_guest::error::HyperlightGuestError;
288/// #[host_function]
289/// fn my_host_function(arg1: i32, arg2: String) -> Result<i32, HyperlightGuestError>;
290/// ```
291#[proc_macro_attribute]
292pub fn host_function(attr: TokenStream, item: TokenStream) -> TokenStream {
293 // Obtain the crate name for hyperlight-guest-bin
294 let crate_name =
295 crate_name("hyperlight-guest-bin").expect("hyperlight-guest-bin must be a dependency");
296 let crate_name = match crate_name {
297 FoundCrate::Itself => quote! {crate},
298 FoundCrate::Name(name) => {
299 let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
300 quote! {::#ident}
301 }
302 };
303
304 // Parse the function declaration that we will be working with, and
305 // early return if parsing as `ForeignItemFn` fails.
306 // A function declaration without a body is a foreign item function, as that's what
307 // you would use when declaring an FFI function.
308 let fn_declaration = parse_macro_input!(item as ForeignItemFn);
309
310 // Destructure the foreign item function to get its components.
311 let ForeignItemFn {
312 attrs,
313 vis,
314 sig,
315 semi_token: _,
316 } = fn_declaration;
317
318 // Obtain the name of the function being decorated.
319 let ident = sig.ident.clone();
320
321 // Determine the name used to call the host function, either
322 // the provided name or the function's identifier.
323 let exported_name = match parse_macro_input!(attr as NameArg) {
324 NameArg::None => quote! { stringify!(#ident) },
325 NameArg::Name(name) => quote! { #name },
326 };
327
328 // Build the list of argument identifiers to pass to the call_host function.
329 // While doing that, also do some sanity checks to improve error messages.
330 // These checks are not strictly necessary, as the generated code would fail
331 // to compile anyway due to either:
332 // * the trait bounds of `call_host`
333 // * the generated code having invalid syntax
334 // but they provide better feedback to the user of the macro, especially in
335 // the case of invalid syntax.
336 let mut args = vec![];
337 for arg in sig.inputs.iter() {
338 match arg {
339 // Reject receiver arguments (i.e., `self`, `&self`, `Box<Self>`, etc).
340 syn::FnArg::Receiver(_) => {
341 return Error::new(
342 arg.span(),
343 "Receiver (self) argument is not allowed in guest functions",
344 )
345 .to_compile_error()
346 .into();
347 }
348 syn::FnArg::Typed(arg) => {
349 // A typed argument: `name: Type`
350 // Technically, the `name` part can be any pattern, e.g., destructuring patterns
351 // like `(a, b): (i32, u64)`, but we only allow simple identifiers here
352 // to keep things simple.
353
354 // Reject anything that is not a simple identifier.
355 let Pat::Ident(pat) = *arg.pat.clone() else {
356 return Error::new(
357 arg.span(),
358 "Only named arguments are allowed in host functions",
359 )
360 .to_compile_error()
361 .into();
362 };
363
364 // Reject any argument with attributes, e.g., `#[cfg(feature = "gdb")] name: Type`
365 if !pat.attrs.is_empty() {
366 return Error::new(
367 arg.span(),
368 "Attributes are not allowed on host function arguments",
369 )
370 .to_compile_error()
371 .into();
372 }
373
374 // Reject any argument passed by reference
375 if pat.by_ref.is_some() {
376 return Error::new(
377 arg.span(),
378 "By-ref arguments are not allowed in host functions",
379 )
380 .to_compile_error()
381 .into();
382 }
383
384 // Reject any mutable argument, e.g., `mut name: Type`
385 if pat.mutability.is_some() {
386 return Error::new(
387 arg.span(),
388 "Mutable arguments are not allowed in host functions",
389 )
390 .to_compile_error()
391 .into();
392 }
393
394 // Reject any sub-patterns
395 if pat.subpat.is_some() {
396 return Error::new(
397 arg.span(),
398 "Sub-patterns are not allowed in host functions",
399 )
400 .to_compile_error()
401 .into();
402 }
403
404 let ident = pat.ident.clone();
405
406 // All checks passed, add the identifier to the argument list.
407 args.push(quote! { #ident });
408 }
409 }
410 }
411
412 // Determine the return type of the function.
413 // If the return type is not specified, it is `()`.
414 let ret: proc_macro2::TokenStream = match &sig.output {
415 syn::ReturnType::Default => quote! { quote! { () } },
416 syn::ReturnType::Type(_, ty) => {
417 quote! { #ty }
418 }
419 };
420
421 // Take the parts of the function declaration and generate a function definition
422 // matching the provided declaration, but with a body that calls the host function.
423 let output = quote! {
424 #(#attrs)* #vis #sig {
425 use #crate_name::__private::FromResult;
426 use #crate_name::host_comm::call_host;
427 <#ret as FromResult>::from_result(call_host(#exported_name, (#(#args,)*)))
428 }
429 };
430
431 output.into()
432}