Skip to main content

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 (cdyn) 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/cdyn) 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            for input in &m.sig.inputs {
87                if let syn::FnArg::Typed(pt) = input {
88                    let ident = match pt.pat.as_ref() {
89                        syn::Pat::Ident(pi) => pi.ident.clone(),
90                        _ => continue,
91                    };
92                    let ty_str = quote!(#pt.ty).to_string().replace(' ', "");
93                    let c_ty = map_type(&pt.ty).unwrap_or_else(|| {
94                        panic!(
95                            "abi_vtable: unsupported param type `{}` in `{}::{}` — only C-representable scalars cross the boundary",
96                            ty_str, trait_name_str, m.sig.ident
97                        )
98                    });
99                    let c_ty: syn::Type = syn::parse_str(c_ty).unwrap();
100                    params.push(quote!(#ident: #c_ty));
101                    call_args.push(ident);
102                }
103            }
104            let ret = match &m.sig.output {
105                syn::ReturnType::Type(_, ty) => {
106                    let ty_str = quote!(#ty).to_string().replace(' ', "");
107                    let c_ty = map_type(ty).unwrap_or_else(|| {
108                        panic!(
109                            "abi_vtable: unsupported return type `{}` in `{}::{}`",
110                            ty_str, trait_name_str, m.sig.ident
111                        )
112                    });
113                    Some(syn::parse_str::<syn::Type>(c_ty).unwrap())
114                }
115                syn::ReturnType::Default => None,
116            };
117            methods.push(Method {
118                name: m.sig.ident.clone(),
119                params,
120                ret,
121                call_args,
122            });
123        }
124    }
125
126    // ---- shadow entities ----
127    let vtable_struct_name = format_ident!("{}Vtable", trait_name);
128
129    let mut struct_fields = Vec::new();
130    // Fully-materialized thunk fns (one token blob per method), consumed by
131    // the generated macro_rules! via a flat single-level repetition.
132    let mut thunks = Vec::new();
133    let mut thunk_assignments = Vec::new();
134
135    for m in &methods {
136        let fname = &m.name;
137        let thunk_name = format_ident!("{}_thunk", m.name);
138        let arg_tys = &m.params;
139        let ret = match &m.ret {
140            Some(t) => quote!(-> #t),
141            None => quote!(),
142        };
143
144        struct_fields.push(quote! {
145            pub #fname: unsafe extern "C" fn(ctx: *mut ::std::ffi::c_void, #(#arg_tys),*) #ret
146        });
147
148        let call_args = &m.call_args;
149        // NOTE: no generics here — `$impl_ty` is substituted by the generated
150        // macro_rules! at expansion time, so thunks are monomorphic by design.
151        thunks.push(quote! {
152            unsafe extern "C" fn #thunk_name(
153                ctx: *mut ::std::ffi::c_void,
154                #(#arg_tys),*
155            ) #ret {
156                unsafe { (&*(ctx as *const $impl_ty)).#fname(#(#call_args),*) }
157            }
158        });
159        thunk_assignments.push(quote! { #fname: #thunk_name });
160    }
161
162    let static_name = format_ident!("{}_VTABLE", opts.name.to_uppercase());
163    let getter_name = format_ident!("{}_get_vtable", opts.name);
164    let instance_name = format_ident!("{}_INSTANCE", opts.name.to_uppercase());
165    let impl_macro = format_ident!("abi_vtable_impl_{}", trait_name_str.to_lowercase());
166
167    let expanded = quote! {
168        #trait_def
169
170        /// Shadow vtable generated from [`#trait_name`] — the C contract.
171        /// Field order IS the protocol; never reorder after release.
172        #[repr(C)]
173        #[derive(Clone, Copy)]
174        #vis struct #vtable_struct_name {
175            #(#struct_fields,)*
176        }
177
178        /// One-line binding: impl type → static instance + vtable + getter.
179        ///
180        /// `$impl_ty` must implement [`#trait_name`]; `$instance` is a
181        /// const-constructible expression (e.g. `MyCalc`).
182        #[macro_export]
183        macro_rules! #impl_macro {
184            // (ImplType, instance_expr)
185            ($impl_ty:ty, $instance:expr) => {
186                static #instance_name: $impl_ty = $instance;
187
188                // Concrete thunks — one per method, bound to $impl_ty.
189                #(
190                    #thunks
191                )*
192
193                static #static_name: #vtable_struct_name = #vtable_struct_name {
194                    #(#thunk_assignments,)*
195                };
196
197                #[no_mangle]
198                pub extern "C" fn #getter_name() -> *const #vtable_struct_name {
199                    &#static_name
200                }
201            };
202        }
203    };
204
205    TokenStream::from(expanded)
206}