ezffi-macros 0.1.1

Proc-macros backing the ezffi crate
Documentation
//! Proc-macros backing the [`ezffi`](https://docs.rs/ezffi) crate. Most users
//! should depend on `ezffi` directly and use [`macro@export`], not this crate
use std::{env, sync::LazyLock};

use quote::quote;
use syn::{Item, parse_macro_input};

use crate::{
    enums::{expand_c_enum, expand_enum, is_c_compatible_enum},
    functions::{WrapMethods, expand_fn, expand_impl, expand_wrap_methods},
    structs::{expand_c_struct, expand_struct, is_c_compatible_struct},
};

mod config;
mod enums;
mod functions;
mod namer;
mod structs;
mod type_resolver;
mod typemap;

use config::CONFIG;
use namer::Namer;
use type_resolver::FFITypeResolver;

static PKG_NAME: LazyLock<String> = LazyLock::new(|| env::var("CARGO_PKG_NAME").unwrap());
static MANIFEST_DIR: LazyLock<String> = LazyLock::new(|| env::var("CARGO_MANIFEST_DIR").unwrap());

const EZFFI_CODEGEN_VERSION: u32 = 1;
const MIN_COMPATIBLE_CODEGEN: u32 = 1;
const CODEGEN_INTRODUCED_IN: &[(u32, &str)] = &[(1, "0.1.0")];

fn crate_version_for_codegen(v: u32) -> &'static str {
    CODEGEN_INTRODUCED_IN
        .iter()
        .find(|(cv, _)| *cv == v)
        .map(|(_, ver)| *ver)
        .unwrap_or("?")
}

/// Expose the annotated item through the FFI layer.
///
/// Applies to `struct`, `enum` and `impl` blocks. The macro
/// decides the right layout and emits the extern wrappers
/// cbindgen will pick up.
///
/// ```ignore
/// #[ezffi::export]
/// pub fn add(a: u32, b: u32) -> u32 { a + b }
///
/// #[ezffi::export]
/// pub struct Point { pub x: f64, pub y: f64 }
///
/// #[ezffi::export]
/// impl Point {
///     pub fn new(x: f64, y: f64) -> Self { Self { x, y } }
/// }
/// ```
///
/// Full reference (which shapes are supported, naming rules, configuration,
/// cross-crate behaviour): [The ezffi Book](https://zocolini.github.io/ezffi/the-macro.html).
#[proc_macro_attribute]
pub fn export(
    attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    export_impl(attr, item, false)
}

/// Registers an external type (one defined outside the current crate, e.g.
/// `std::string::String` or `Vec<T>`) as opaque-pointer-exported. Used
/// internally by `ezffi`'s std prelude; downstream users cannot use it
/// unless they define the same traits this macro expands to
#[proc_macro]
pub fn export_extern_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let path = parse_macro_input!(input as syn::Path);

    let dummy_struct = quote! { struct #path {} }.into();

    export_impl(proc_macro::TokenStream::new(), dummy_struct, true)
}

/// Wraps a list of methods on an externally-defined type with C-FFI shims.
/// Used by `ezffi` to expose Rust std struct methods to C.
///
/// ```ignore
/// wrap_methods!(String {
///     fn new() -> String;
///     fn len(&self) -> usize;
///     fn push_str(&mut self, s: &str);
/// });
/// ```
///
/// Not meant for downstream users (yet). It may be made public in a future
/// release, probably under a different name
#[proc_macro]
pub fn wrap_methods(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let parsed = parse_macro_input!(input as WrapMethods);

    let result = FFITypeResolver::check_registry().and_then(|()| expand_wrap_methods(&parsed));

    match result {
        Ok(ts) => ts.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn export_impl(
    _attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
    skip_input: bool,
) -> proc_macro::TokenStream {
    let item = parse_macro_input!(item as Item);

    match expand_item(&item, skip_input) {
        Ok(ts) => ts.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn expand_item(item: &Item, mut skip_input: bool) -> syn::Result<proc_macro2::TokenStream> {
    FFITypeResolver::check_registry()?;

    let output = match item {
        Item::Struct(item) => {
            if item.generics.gt_token.is_none() && is_c_compatible_struct(item) {
                skip_input = true;
                expand_c_struct(item)
            } else {
                expand_struct(item)?
            }
        }
        Item::Enum(item) => {
            if is_c_compatible_enum(item) {
                skip_input = true;
                expand_c_enum(item)
            } else {
                expand_enum(item)?
            }
        }
        Item::Fn(item) => expand_fn(item)?,
        Item::Impl(item) => expand_impl(item)?,
        other => {
            return Err(syn::Error::new_spanned(
                other,
                format!(
                    "#[ezffi::export] doesn't support this item: `{}`",
                    quote!(#other)
                ),
            ));
        }
    };

    if skip_input {
        Ok(output)
    } else {
        Ok(quote! {
            #item
            #output
        })
    }
}