nub_rt_macro/lib.rs
1//! Procedural macros for declaring nub_rt guest endpoints.
2//!
3//! The `#[nub_rt::endpoint(N)]` attribute marks a function as
4//! endpoint `N` of a PVM2 program. The macro emits three items
5//! into the guest crate:
6//!
7//! 1. The function definition itself, unchanged.
8//! 2. A per-endpoint trampoline `__nub_rt_ep_N_trampoline` in
9//! `.text` that calls the user function, then halts the VM via
10//! a bare `ecall` with no CSR marker, which the linker rewrites
11//! to `custom-0 ecalli imm=0` — the clean-halt convention. The
12//! trampoline lives in regular code; the engine enters it at
13//! `endpoints[N].entry_pc`.
14//! 3. A `nub_rt::EndpointDescriptor` static in the
15//! `.nub.endpoints` ELF section whose `fn_ptr` points at
16//! the trampoline (not the user fn). `nub-linker` reads the
17//! section at link time and resolves each `fn_ptr` to a PVM PC.
18//!
19//! ```ignore
20//! #[nub_rt::endpoint(0)]
21//! fn process(args_len: u64) -> u64 { ... }
22//! ```
23//!
24//! On host targets the macro emits only the function definition;
25//! the trampoline and descriptor are gated behind
26//! `cfg(all(target_os = "none", target_arch = "riscv64"))`.
27
28use proc_macro::TokenStream;
29use quote::{format_ident, quote};
30use syn::{ItemFn, LitInt, parse_macro_input};
31
32/// Mark a function as endpoint `N` of a PVM2 program.
33///
34/// `N` must be a `u8` literal (0..=255). Validates the function
35/// signature loosely; the linker does the strict check when it
36/// resolves the descriptor against the ELF symbol table.
37#[proc_macro_attribute]
38pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
39 let idx = parse_macro_input!(attr as LitInt);
40 let idx_value: u8 = match idx.base10_parse::<u8>() {
41 Ok(v) => v,
42 Err(e) => return e.to_compile_error().into(),
43 };
44
45 let func = parse_macro_input!(item as ItemFn);
46 let fn_name = &func.sig.ident;
47 let descriptor_name = format_ident!("__NUB_RT_ENDPOINT_{}", idx_value);
48 let trampoline_ident = format_ident!("__nub_rt_ep_{}_trampoline", idx_value);
49 let trampoline_label = format!("__nub_rt_ep_{}_trampoline", idx_value);
50 let global_directive = format!(".global {trampoline_label}");
51 let trampoline_label_colon = format!("{trampoline_label}:");
52
53 let expanded = quote! {
54 #func
55
56 #[cfg(all(target_os = "none", target_arch = "riscv64"))]
57 core::arch::global_asm!(
58 ".text",
59 #global_directive,
60 #trampoline_label_colon,
61 "call {user_fn}",
62 // Bare `ecall` (no CSR marker) -> the linker rewrites it
63 // to `custom-0 ecalli imm=0`, the clean-halt convention.
64 "li t0, 0",
65 "ecall",
66 "unimp", // trap if somehow resumed after REPLY
67 user_fn = sym #fn_name,
68 );
69
70 #[cfg(all(target_os = "none", target_arch = "riscv64"))]
71 unsafe extern "Rust" {
72 safe fn #trampoline_ident(args_len: u64) -> u64;
73 }
74
75 #[cfg(all(target_os = "none", target_arch = "riscv64"))]
76 #[doc(hidden)]
77 #[unsafe(link_section = ".nub.endpoints")]
78 #[used]
79 static #descriptor_name: ::nub_rt::EndpointDescriptor =
80 ::nub_rt::EndpointDescriptor {
81 fn_ptr: #trampoline_ident,
82 index: #idx_value,
83 arg_registers: 0,
84 arg_meta: 0,
85 _pad: [0; 5],
86 };
87 };
88
89 expanded.into()
90}