use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, ItemTrait, TraitItem};
fn map_type(ty: &syn::Type) -> Option<&'static str> {
let ty_str = quote!(#ty).to_string().replace(' ', "");
Some(match ty_str.as_str() {
"i8" => "i8",
"i16" => "i16",
"i32" => "i32",
"i64" => "i64",
"u8" => "u8",
"u16" => "u16",
"u32" => "u32",
"u64" => "u64",
"f32" => "f32",
"f64" => "f64",
"bool" => "bool",
"usize" => "usize",
"isize" => "isize",
_ => return None,
})
}
struct AttrOpts {
name: String,
}
fn parse_attr_tokens(ts: proc_macro2::TokenStream) -> AttrOpts {
let mut name = String::new();
let mut iter = ts.into_iter().peekable();
while let Some(t) = iter.next() {
if let proc_macro2::TokenTree::Ident(id) = t {
if id == "name" {
for t2 in iter.by_ref() {
if let proc_macro2::TokenTree::Literal(lit) = t2 {
name = lit.to_string().trim_matches('"').to_string();
break;
}
}
}
}
}
if name.is_empty() {
panic!("#[abi_vtable] requires `name = \"...\"` (getter symbol prefix)");
}
AttrOpts { name }
}
#[proc_macro_attribute]
pub fn abi_vtable(attr: TokenStream, item: TokenStream) -> TokenStream {
let opts = parse_attr_tokens(attr.into());
let trait_def = parse_macro_input!(item as ItemTrait);
let trait_name = &trait_def.ident;
let trait_name_str = trait_name.to_string();
let vis = &trait_def.vis;
struct Method {
name: syn::Ident,
params: Vec<proc_macro2::TokenStream>, ret: Option<syn::Type>,
call_args: Vec<syn::Ident>,
}
let mut methods: Vec<Method> = Vec::new();
for item in &trait_def.items {
if let TraitItem::Fn(m) = item {
let mut params = Vec::new();
let mut call_args = Vec::new();
for input in &m.sig.inputs {
if let syn::FnArg::Typed(pt) = input {
let ident = match pt.pat.as_ref() {
syn::Pat::Ident(pi) => pi.ident.clone(),
_ => continue,
};
let ty_str = quote!(#pt.ty).to_string().replace(' ', "");
let c_ty = map_type(&pt.ty).unwrap_or_else(|| {
panic!(
"abi_vtable: unsupported param type `{}` in `{}::{}` — only C-representable scalars cross the boundary",
ty_str, trait_name_str, m.sig.ident
)
});
let c_ty: syn::Type = syn::parse_str(c_ty).unwrap();
params.push(quote!(#ident: #c_ty));
call_args.push(ident);
}
}
let ret = match &m.sig.output {
syn::ReturnType::Type(_, ty) => {
let ty_str = quote!(#ty).to_string().replace(' ', "");
let c_ty = map_type(ty).unwrap_or_else(|| {
panic!(
"abi_vtable: unsupported return type `{}` in `{}::{}`",
ty_str, trait_name_str, m.sig.ident
)
});
Some(syn::parse_str::<syn::Type>(c_ty).unwrap())
}
syn::ReturnType::Default => None,
};
methods.push(Method {
name: m.sig.ident.clone(),
params,
ret,
call_args,
});
}
}
let vtable_struct_name = format_ident!("{}Vtable", trait_name);
let mut struct_fields = Vec::new();
let mut thunks = Vec::new();
let mut thunk_assignments = Vec::new();
for m in &methods {
let fname = &m.name;
let thunk_name = format_ident!("{}_thunk", m.name);
let arg_tys = &m.params;
let ret = match &m.ret {
Some(t) => quote!(-> #t),
None => quote!(),
};
struct_fields.push(quote! {
pub #fname: unsafe extern "C" fn(ctx: *mut ::std::ffi::c_void, #(#arg_tys),*) #ret
});
let call_args = &m.call_args;
thunks.push(quote! {
unsafe extern "C" fn #thunk_name(
ctx: *mut ::std::ffi::c_void,
#(#arg_tys),*
) #ret {
unsafe { (&*(ctx as *const $impl_ty)).#fname(#(#call_args),*) }
}
});
thunk_assignments.push(quote! { #fname: #thunk_name });
}
let static_name = format_ident!("{}_VTABLE", opts.name.to_uppercase());
let getter_name = format_ident!("{}_get_vtable", opts.name);
let instance_name = format_ident!("{}_INSTANCE", opts.name.to_uppercase());
let impl_macro = format_ident!("abi_vtable_impl_{}", trait_name_str.to_lowercase());
let expanded = quote! {
#trait_def
#[repr(C)]
#[derive(Clone, Copy)]
#vis struct #vtable_struct_name {
#(#struct_fields,)*
}
#[macro_export]
macro_rules! #impl_macro {
($impl_ty:ty, $instance:expr) => {
static #instance_name: $impl_ty = $instance;
#(
#thunks
)*
static #static_name: #vtable_struct_name = #vtable_struct_name {
#(#thunk_assignments,)*
};
#[no_mangle]
pub extern "C" fn #getter_name() -> *const #vtable_struct_name {
&#static_name
}
};
}
};
TokenStream::from(expanded)
}