use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{Attribute, Error, Expr, Ident, Item, ItemImpl, ItemMod, Path, Type, parse_quote};
use crate::args::ImplArgs;
pub(crate) fn expand_module(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
if !attr.is_empty() {
return Err(Error::new_spanned(
attr,
"`#[xabi::module]` does not accept options",
));
}
let mut item_mod = syn::parse2::<ItemMod>(item)?;
let Some((_, items)) = item_mod.content.as_mut() else {
return Err(Error::new_spanned(
item_mod,
"`#[xabi::module]` requires an inline module",
));
};
let mut exports = Vec::new();
for item in items.iter_mut() {
let Item::Impl(item_impl) = item else {
continue;
};
let Some(args) = take_xabi_impl_attr(&mut item_impl.attrs)? else {
continue;
};
exports.push(ModuleExport::new(exports.len(), item_impl, args)?);
}
let export_count = exports.len();
let make_fns = exports.iter().map(|export| export.make_fn());
let export_entries = exports.iter().map(|export| export.entry());
items.push(parse_quote! {
#[unsafe(no_mangle)]
pub extern "C" fn xabi_manifest() -> *const ::xabi::XabiManifest {
&XABI_MANIFEST
}
});
items.push(parse_quote! {
static XABI_EXPORTS: [::xabi::XabiExport; #export_count] = [
#(#export_entries,)*
];
});
items.push(parse_quote! {
static XABI_MANIFEST: ::xabi::XabiManifest = ::xabi::XabiManifest::new(&XABI_EXPORTS);
});
for make_fn in make_fns {
items.push(syn::parse2(make_fn)?);
}
Ok(quote!(#item_mod))
}
struct ModuleExport {
make_fn_ident: Ident,
trait_path: Path,
impl_ty: Type,
name: Expr,
version: Expr,
constructor: Option<Expr>,
}
impl ModuleExport {
fn new(index: usize, item_impl: &ItemImpl, args: ImplArgs) -> syn::Result<Self> {
let trait_path = impl_trait_path(item_impl)?;
let impl_ty = (*item_impl.self_ty).clone();
let make_fn_ident = make_export_ident(index, &trait_path, &impl_ty);
Ok(Self {
make_fn_ident,
trait_path,
impl_ty,
name: args.name,
version: args.version,
constructor: args.constructor,
})
}
fn make_fn(&self) -> TokenStream2 {
let make_fn_ident = &self.make_fn_ident;
let trait_path = &self.trait_path;
let impl_ty = &self.impl_ty;
let constructor = self
.constructor
.as_ref()
.map(|constructor| quote!((#constructor)()))
.unwrap_or_else(|| quote!(<#impl_ty as Default>::default()));
quote! {
#[allow(non_snake_case)]
unsafe extern "C" fn #make_fn_ident() -> *mut std::ffi::c_void {
<#impl_ty as #trait_path>::__xabi_export(#constructor)
}
}
}
fn entry(&self) -> TokenStream2 {
let make_fn_ident = &self.make_fn_ident;
let trait_path = &self.trait_path;
let impl_ty = &self.impl_ty;
let name = &self.name;
let version = &self.version;
quote! {
::xabi::XabiExport::new(
::xabi::XabiStr::from_static(
<#impl_ty as #trait_path>::__XABI_ID,
),
::xabi::XabiStr::from_static(#name),
<#impl_ty as #trait_path>::__XABI_VERSION,
#version,
::xabi::CAP_NONE,
#make_fn_ident,
)
}
}
}
fn make_export_ident(index: usize, trait_path: &Path, impl_ty: &Type) -> Ident {
let Some(trait_ident) = trait_path.segments.last().map(|segment| &segment.ident) else {
return format_ident!("__xabi_make_export_{index}");
};
let Some(impl_ident) = type_last_ident(impl_ty) else {
return format_ident!("__xabi_make_export_{index}");
};
format_ident!("XabiV1Trait{}Impl{}", trait_ident, impl_ident)
}
fn type_last_ident(ty: &Type) -> Option<&Ident> {
let Type::Path(path) = ty else {
return None;
};
path.path.segments.last().map(|segment| &segment.ident)
}
fn take_xabi_impl_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Option<ImplArgs>> {
let Some(index) = attrs.iter().position(is_xabi_attr) else {
return Ok(None);
};
let attr = attrs.remove(index);
attr.parse_args::<ImplArgs>().map(Some)
}
fn is_xabi_attr(attr: &Attribute) -> bool {
let path = attr.path();
path.segments
.last()
.map(|segment| segment.ident == "xabi")
.unwrap_or(false)
}
fn impl_trait_path(item_impl: &ItemImpl) -> syn::Result<Path> {
let Some((_, path, _)) = &item_impl.trait_ else {
return Err(Error::new_spanned(
item_impl,
"xabi implementation exports must implement a trait",
));
};
Ok(path.clone())
}