ezffi-macros 0.1.1

Proc-macros backing the ezffi crate
Documentation
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{FnArg, ItemFn, ItemImpl, ReturnType, Signature, Type};

use crate::{FFITypeResolver, Namer};

pub fn expand_impl(item: &ItemImpl) -> syn::Result<proc_macro2::TokenStream> {
    let impl_ty = &item.self_ty;

    let mut wrappers = Vec::new();

    for impl_item in &item.items {
        if let syn::ImplItem::Fn(method) = impl_item {
            wrappers.push(generate_fn_wrapper(Some(impl_ty), &method.sig)?);
        }
    }

    Ok(quote! { #( #wrappers )* })
}

pub fn expand_fn(item: &ItemFn) -> syn::Result<proc_macro2::TokenStream> {
    generate_fn_wrapper(None, &item.sig)
}

pub struct WrapMethods {
    ty: Type,
    sigs: Vec<Signature>,
}

impl Parse for WrapMethods {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let ty: Type = input.parse()?;

        let content;
        syn::braced!(content in input);

        let mut sigs = Vec::new();
        while !content.is_empty() {
            // Tolerate `pub fn ...` so signatures can be pasted from std docs.
            let _: syn::Visibility = content.parse()?;
            sigs.push(content.parse()?);
            content.parse::<syn::Token![;]>()?;
        }

        Ok(Self { ty, sigs })
    }
}

pub fn expand_wrap_methods(item: &WrapMethods) -> syn::Result<proc_macro2::TokenStream> {
    let wrappers: Vec<_> = item
        .sigs
        .iter()
        .map(|sig| generate_fn_wrapper(Some(&item.ty), sig))
        .collect::<syn::Result<Vec<_>>>()?;

    Ok(quote! { #( #wrappers )* })
}

fn generate_fn_wrapper(
    impl_ty: Option<&Type>,
    sig: &Signature,
) -> syn::Result<proc_macro2::TokenStream> {
    let fn_name = &sig.ident;
    let inputs = sig.inputs.iter().collect::<Vec<_>>();
    let output = &sig.output;
    let is_async = sig.asyncness.is_some();

    let mut c_args = Vec::new();
    let mut c_args_to_rust = Vec::new();
    let mut rust_args = Vec::new();

    // Generate the FFI function name
    let ffi_fn_name = Namer::name_fn(fn_name, impl_ty)?;

    // Convert Rust args into C args and C-types into Rust-types
    // using the ezffi traits
    for arg in inputs {
        match arg {
            FnArg::Receiver(receiver) => {
                let c_ty = super::FFITypeResolver::c_type_of(&receiver.ty, impl_ty)?;

                let is_ref = receiver.reference.is_some();
                let is_mut = receiver.mutability.is_some();

                let (ffi_self_ty, self_conversion) = match (is_ref, is_mut) {
                    (false, _) => (
                        quote! { #c_ty },
                        quote! { let mut this = this.into_rust_owned(); },
                    ),
                    (true, false) => (
                        quote! { *const #c_ty },
                        quote! {
                            let mut this = &*this;
                            let this = this.into_rust();
                        },
                    ),
                    (true, true) => (
                        quote! { *mut #c_ty },
                        quote! {
                            let this = &mut *this;
                            let this = this.into_rust_mut();
                        },
                    ),
                };

                c_args.push(quote! { mut this: #ffi_self_ty });
                c_args_to_rust.push(self_conversion);
                rust_args.push(quote! { this });
            }
            FnArg::Typed(pat_type) => {
                let name = match &*pat_type.pat {
                    syn::Pat::Ident(ident) => &ident.ident,
                    other => {
                        return Err(syn::Error::new_spanned(
                            other,
                            "unsupported parameter pattern in #[ezffi::export]",
                        ));
                    }
                };
                let ty = &pat_type.ty;

                // This two cases are sent by value and dont need ty conversion
                if FFITypeResolver::is_primitive(ty) || FFITypeResolver::is_c_callback(ty) {
                    c_args.push(quote! { mut #name: #ty });
                } else {
                    let c_ty = super::FFITypeResolver::c_type_of(ty, impl_ty)?;

                    let (c_ty_as_arg, c_arg_to_rust) = match &*pat_type.ty {
                        Type::Reference(r) => {
                            // This is the special case were we have fat pointers as args, they don't follow
                            // the Rust signature and are always sent by value
                            let c_ty_str = c_ty.to_string().replace(' ', "");
                            if c_ty_str == "::ezffi::EzffiSlice" || c_ty_str == "::ezffi::EzffiStr"
                            {
                                (
                                    quote! { #c_ty },
                                    if r.mutability.is_some() {
                                        quote! { let #name = #name.into_rust_mut(); }
                                    } else {
                                        quote! { let #name = #name.into_rust(); }
                                    },
                                )
                            } else if r.mutability.is_some() {
                                (
                                    quote! { *mut #c_ty },
                                    quote! {
                                        let mut #name = &mut *#name;
                                        let mut #name = #name.into_rust_mut();
                                    },
                                )
                            } else {
                                (
                                    quote! { *const #c_ty },
                                    quote! {
                                        let #name = &*#name;
                                        let #name = #name.into_rust();
                                    },
                                )
                            }
                        }
                        Type::Path(_) => (
                            quote! { #c_ty },
                            quote! { let mut #name = #name.into_rust_owned(); },
                        ),
                        _ => {
                            return Err(syn::Error::new_spanned(
                                ty,
                                format!(
                                    "unsupported parameter type in #[ezffi::export]: `{}`",
                                    quote!(#ty)
                                ),
                            ));
                        }
                    };

                    c_args.push(quote! { mut #name: #c_ty_as_arg });
                    c_args_to_rust.push(c_arg_to_rust);
                }

                rust_args.push(quote! { #name });
            }
        }
    }

    // Call the function using full qualified name, have to check
    // if it is a method or a function
    let rust_call = if let Some(ty) = impl_ty {
        match ty {
            syn::Type::Path(path) => {
                let ident = &path.path.segments[0].ident;
                quote! { #ident::#fn_name( #( #rust_args ),* ) }
            }
            other => {
                return Err(syn::Error::new_spanned(
                    other,
                    format!("cannot wrap a method on impl target `{}`", quote!(#other)),
                ));
            }
        }
    } else {
        quote! { #fn_name( #( #rust_args ),* ) }
    };

    let rust_call = call_wrapper(is_async, sig, rust_call)?;

    // Function return type
    let c_ret_ty = match output {
        ReturnType::Default => quote! { () },
        ReturnType::Type(_, ty) => {
            let ty = FFITypeResolver::c_type_of(ty, impl_ty)?;
            quote! { #ty }
        }
    };

    // Return conversion
    let rust_ret_to_c = match output {
        ReturnType::Default => quote! {},
        ReturnType::Type(_, ty) => from_rust_to_c_call(ty)?,
    };

    Ok(quote! {
        #[doc(hidden)]
        #[inline]
        #[unsafe(no_mangle)]
        pub unsafe extern "C" fn #ffi_fn_name(
            #( #c_args ),*
        ) -> #c_ret_ty {
            use ezffi::RustRefIntoC;
            use ezffi::RustOwnedIntoC;
            use ezffi::CRefIntoRust;
            use ezffi::COwnedIntoRust;

            #( #c_args_to_rust )*

            let result = #rust_call;

            #rust_ret_to_c
        }
    })
}

fn from_rust_to_c_call(ty: &syn::Type) -> syn::Result<proc_macro2::TokenStream> {
    if FFITypeResolver::is_primitive(ty) || FFITypeResolver::is_c_callback(ty) {
        Ok(quote! { result })
    } else {
        match ty {
            syn::Type::Reference(_) => Ok(quote! { result.ref_into_c() }),
            syn::Type::Path(_) => Ok(quote! { result.owned_into_c() }),
            _ => Err(syn::Error::new_spanned(
                ty,
                format!(
                    "unsupported return type in #[ezffi::export]: `{}`",
                    quote!(#ty)
                ),
            )),
        }
    }
}

#[cfg(feature = "async")]
fn call_wrapper(
    is_async: bool,
    _sig: &Signature,
    call: proc_macro2::TokenStream,
) -> syn::Result<proc_macro2::TokenStream> {
    if is_async {
        Ok(quote! { ::ezffi::dispatch_async(#call) })
    } else {
        Ok(call)
    }
}

#[cfg(not(feature = "async"))]
fn call_wrapper(
    is_async: bool,
    sig: &Signature,
    call: proc_macro2::TokenStream,
) -> syn::Result<proc_macro2::TokenStream> {
    if is_async {
        let span = sig
            .asyncness
            .as_ref()
            .map(|a| a.span)
            .unwrap_or_else(proc_macro2::Span::call_site);
        return Err(syn::Error::new(
            span,
            "async functions require enabling the `async` feature on ezffi",
        ));
    }
    Ok(call)
}