abi_vtable_macro/lib.rs
1//! # abi-vtable-macro — shadow-mode vtable generation
2//!
3//! **Shadow mode**: annotate a trait and every companion entity of the
4//! abi-mode protocol is generated automatically — no manual steps.
5//! Usage is as frictionless as the native (dyn) mode.
6//!
7//! ## When to use which mode
8//! - **Rust-only** → native (dyn) mode: fastest and lightest, direct fat
9//! pointers, Arc ref-counting, no vtable struct at all.
10//! - **Cross-language (C/C++/Zig/…)** → this (abi) mode: the generated
11//! `#[repr(C)]` vtable is the language-neutral contract.
12
13use proc_macro::TokenStream;
14use quote::{format_ident, quote};
15use syn::{parse_macro_input, ItemTrait, TraitItem};
16
17/// Map a Rust type to its C-representable counterpart.
18/// Only types with a stable C representation are supported.
19fn map_type(ty: &syn::Type) -> Option<&'static str> {
20 let ty_str = quote!(#ty).to_string().replace(' ', "");
21 Some(match ty_str.as_str() {
22 "i8" => "i8",
23 "i16" => "i16",
24 "i32" => "i32",
25 "i64" => "i64",
26 "u8" => "u8",
27 "u16" => "u16",
28 "u32" => "u32",
29 "u64" => "u64",
30 "f32" => "f32",
31 "f64" => "f64",
32 "bool" => "bool",
33 "usize" => "usize",
34 "isize" => "isize",
35 _ => return None,
36 })
37}
38
39struct AttrOpts {
40 /// Export prefix, e.g. "calc" → `calc_get_vtable`, `CALC_VTABLE`.
41 name: String,
42}
43
44fn parse_attr_tokens(ts: proc_macro2::TokenStream) -> AttrOpts {
45 let mut name = String::new();
46 let mut iter = ts.into_iter().peekable();
47 while let Some(t) = iter.next() {
48 if let proc_macro2::TokenTree::Ident(id) = t {
49 if id == "name" {
50 for t2 in iter.by_ref() {
51 if let proc_macro2::TokenTree::Literal(lit) = t2 {
52 name = lit.to_string().trim_matches('"').to_string();
53 break;
54 }
55 }
56 }
57 }
58 }
59 if name.is_empty() {
60 panic!("#[abi_vtable] requires `name = \"...\"` (getter symbol prefix)");
61 }
62 AttrOpts { name }
63}
64
65#[proc_macro_attribute]
66pub fn abi_vtable(attr: TokenStream, item: TokenStream) -> TokenStream {
67 let opts = parse_attr_tokens(attr.into());
68 let trait_def = parse_macro_input!(item as ItemTrait);
69 let trait_name = &trait_def.ident;
70 let trait_name_str = trait_name.to_string();
71 let vis = &trait_def.vis;
72
73 // ---- collect &self methods with C-representable signatures ----
74 struct Method {
75 name: syn::Ident,
76 params: Vec<proc_macro2::TokenStream>, // "id: CTy"
77 ret: Option<syn::Type>,
78 call_args: Vec<syn::Ident>,
79 }
80
81 let mut methods: Vec<Method> = Vec::new();
82 for item in &trait_def.items {
83 if let TraitItem::Fn(m) = item {
84 let mut params = Vec::new();
85 let mut call_args = Vec::new();
86 // Receiver: only `&self` is supported. The thunk dispatches on
87 // the module-side static instance behind a shared reference —
88 // `&mut self` cannot be honored across the C boundary (the host
89 // holds no exclusive borrow). Use interior mutability (atomics,
90 // locks) in the impl instead. `self` by value makes no sense
91 // across the boundary either.
92 match m.sig.inputs.first() {
93 Some(syn::FnArg::Receiver(r)) => match &r.reference {
94 Some(_) => {
95 if r.mutability.is_some() {
96 panic!(
97 "abi_vtable: `{}::{}` takes `&mut self` — not expressible across the C ABI; use `&self` + interior mutability",
98 trait_name_str, m.sig.ident
99 );
100 }
101 false
102 }
103 None => panic!(
104 "abi_vtable: `{}::{}` takes `self` by value — use `&self`",
105 trait_name_str, m.sig.ident
106 ),
107 },
108 _ => panic!(
109 "abi_vtable: `{}::{}` has no `self` receiver — associated functions are not vtable methods",
110 trait_name_str, m.sig.ident
111 ),
112 };
113 for input in &m.sig.inputs {
114 if let syn::FnArg::Typed(pt) = input {
115 let ident = match pt.pat.as_ref() {
116 syn::Pat::Ident(pi) => pi.ident.clone(),
117 _ => continue,
118 };
119 let ty_str = quote!(#pt.ty).to_string().replace(' ', "");
120 let c_ty = map_type(&pt.ty).unwrap_or_else(|| {
121 panic!(
122 "abi_vtable: unsupported param type `{}` in `{}::{}` — only C-representable scalars cross the boundary",
123 ty_str, trait_name_str, m.sig.ident
124 )
125 });
126 let c_ty: syn::Type = syn::parse_str(c_ty).unwrap();
127 params.push(quote!(#ident: #c_ty));
128 call_args.push(ident);
129 }
130 }
131 let ret = match &m.sig.output {
132 syn::ReturnType::Type(_, ty) => {
133 let ty_str = quote!(#ty).to_string().replace(' ', "");
134 let c_ty = map_type(ty).unwrap_or_else(|| {
135 panic!(
136 "abi_vtable: unsupported return type `{}` in `{}::{}`",
137 ty_str, trait_name_str, m.sig.ident
138 )
139 });
140 Some(syn::parse_str::<syn::Type>(c_ty).unwrap())
141 }
142 syn::ReturnType::Default => None,
143 };
144 methods.push(Method {
145 name: m.sig.ident.clone(),
146 params,
147 ret,
148 call_args,
149 });
150 }
151 }
152
153 // ---- shadow entities ----
154 let vtable_struct_name = format_ident!("{}Vtable", trait_name);
155 let instance_name = format_ident!("{}_INSTANCE", opts.name.to_uppercase());
156
157 let mut struct_fields = Vec::new();
158 // Fully-materialized thunk fns (one token blob per method), consumed by
159 // the generated macro_rules! via a flat single-level repetition.
160 let mut thunks = Vec::new();
161 let mut thunk_assignments = Vec::new();
162
163 for m in &methods {
164 let fname = &m.name;
165 // Prefix thunks with the export name so two traits in the same crate
166 // with same-named methods never collide (e.g. `alpha_value_thunk`).
167 let thunk_name = format_ident!("{}_{}_thunk", opts.name, m.name);
168 let arg_tys = &m.params;
169 let ret = match &m.ret {
170 Some(t) => quote!(-> #t),
171 None => quote!(),
172 };
173
174 struct_fields.push(quote! {
175 pub #fname: unsafe extern "C" fn(ctx: *mut ::std::ffi::c_void, #(#arg_tys),*) #ret
176 });
177
178 let call_args = &m.call_args;
179 // NOTE: no generics here — `$impl_ty` is substituted by the generated
180 // macro_rules! at expansion time, so thunks are monomorphic by design.
181 // Fully-qualified call: if $impl_ty implements several traits with
182 // same-named methods, a bare `.method()` call would be ambiguous.
183 //
184 // The receiver is the macro-generated static INSTANCE (const-
185 // constructible on the module side), NOT the ctx pointer: the host
186 // has no way to obtain the module's instance address, and stateless
187 // vtables legitimately pass null ctx. `&mut self` methods go through
188 // interior mutability on the module side (the static is `&$impl_ty`;
189 // mutation is the impl's responsibility, e.g. atomics).
190 thunks.push(quote! {
191 unsafe extern "C" fn #thunk_name(
192 ctx: *mut ::std::ffi::c_void,
193 #(#arg_tys),*
194 ) #ret {
195 let _ = ctx; // instance lives in the module; ctx is reserved
196 unsafe { <$impl_ty as #trait_name>::#fname(&*#instance_name, #(#call_args),*) }
197 }
198 });
199 thunk_assignments.push(quote! { #fname: #thunk_name });
200 }
201
202 let static_name = format_ident!("{}_VTABLE", opts.name.to_uppercase());
203 let getter_name = format_ident!("{}_get_vtable", opts.name);
204 let impl_macro = format_ident!("abi_vtable_impl_{}", trait_name_str.to_lowercase());
205
206 // ---- host-side safe wrapper ----
207 // A `{Name}Host` struct (`calc` → `CalcHost`) holding the vtable pointer
208 // + ctx, exposing one safe method per trait method. The host applies the
209 // same `#[abi_vtable]` to the shared trait declaration and gets this
210 // wrapper for free — no raw `unsafe` fn-pointer calls required.
211 let host_wrapper_name_str = capitalize(&opts.name);
212 let host_wrapper = format_ident!("{}Host", host_wrapper_name_str);
213
214 let mut host_methods = Vec::new();
215 for m in &methods {
216 let fname = &m.name;
217 let arg_tys = &m.params;
218 let ret = match &m.ret {
219 Some(t) => quote!(-> #t),
220 None => quote!(),
221 };
222 let call_args = &m.call_args;
223 host_methods.push(quote! {
224 pub fn #fname(&self, #(#arg_tys),*) #ret {
225 unsafe { ((*self.vtable).#fname)(self.ctx, #(#call_args),*) }
226 }
227 });
228 }
229
230 let expanded = quote! {
231 #trait_def
232
233 /// Shadow vtable generated from [`#trait_name`] — the C contract.
234 /// Field order IS the protocol; never reorder after release.
235 #[repr(C)]
236 #[derive(Clone, Copy)]
237 #vis struct #vtable_struct_name {
238 #(#struct_fields,)*
239 }
240
241 /// Host-side safe wrapper generated from [`#trait_name`].
242 ///
243 /// Holds the vtable pointer plus the instance context; exposes one
244 /// safe method per trait method, so host code never writes `unsafe`
245 /// fn-pointer calls. Build it from a loaded vtable:
246 ///
247 /// ```ignore
248 /// let module = unsafe { AbiTable::<#vtable_struct_name>::load(path, b"...\0")? };
249 /// let host = #host_wrapper::new(module.vtable(), std::ptr::null_mut());
250 /// ```
251 ///
252 /// # Safety contract
253 ///
254 /// The vtable pointer and ctx must remain valid for as long as this
255 /// wrapper exists — keep the library handle alive. Methods are safe
256 /// because the generated thunks are plain C functions over C scalars.
257 #vis struct #host_wrapper<'a> {
258 vtable: &'a #vtable_struct_name,
259 ctx: *mut ::std::ffi::c_void,
260 }
261
262 // SAFETY: the wrapper only reads the vtable (immutable reference) and
263 // passes ctx to extern "C" thunks over C scalars. Thread-safety of the
264 // underlying instance is the module's declared guarantee (same policy
265 // as the fn-pointer struct itself).
266 unsafe impl Send for #host_wrapper<'_> {}
267 unsafe impl Sync for #host_wrapper<'_> {}
268
269 impl<'a> #host_wrapper<'a> {
270 /// Wrap a loaded vtable. `ctx` is the instance handle received
271 /// from the module (`null` for stateless tables).
272 pub fn new(
273 vtable: &'a #vtable_struct_name,
274 ctx: *mut ::std::ffi::c_void,
275 ) -> Self {
276 Self { vtable, ctx }
277 }
278
279 /// The underlying vtable (for hand-written fallback calls).
280 pub fn vtable(&self) -> &#vtable_struct_name {
281 self.vtable
282 }
283
284 /// The instance context pointer (first argument of every call).
285 pub fn ctx(&self) -> *mut ::std::ffi::c_void {
286 self.ctx
287 }
288
289 #(#host_methods)*
290 }
291
292 /// One-line binding: impl type → static instance + vtable + getter.
293 ///
294 /// `$impl_ty` must implement [`#trait_name`]; `$instance` is a
295 /// const-constructible expression (e.g. `MyCalc`).
296 #[macro_export]
297 macro_rules! #impl_macro {
298 // (ImplType, instance_expr)
299 //
300 // `$instance` is a const-constructible expression. The generated
301 // static holds a *reference* (`static INSTANCE: &$impl_ty =
302 // &$instance;`) — with a unit-struct / const-expression argument
303 // Rust's static promotion makes this a single shared instance,
304 // so the ctx pointer the host passes and this static alias the
305 // same object. Non-Copy types are fine (nothing is moved).
306 ($impl_ty:ty, $instance:expr) => {
307 static #instance_name: &$impl_ty = &$instance;
308
309 // Concrete thunks — one per method, bound to $impl_ty.
310 #(
311 #thunks
312 )*
313
314 static #static_name: #vtable_struct_name = #vtable_struct_name {
315 #(#thunk_assignments,)*
316 };
317
318 #[no_mangle]
319 pub extern "C" fn #getter_name() -> *const #vtable_struct_name {
320 &#static_name
321 }
322 };
323 }
324 };
325
326 TokenStream::from(expanded)
327}
328
329/// Uppercase the first character of an ASCII identifier (`calc` → `Calc`).
330fn capitalize(s: &str) -> String {
331 let mut chars = s.chars();
332 match chars.next() {
333 Some(c) => c.to_ascii_uppercase().to_string() + chars.as_str(),
334 None => String::new(),
335 }
336}