uniffi_macros 0.32.0

a multi-language bindings generator for rust (convenience macros)
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::fnsig::FnSignature;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::ToTokens;

use super::attributes::{
    ExportFnArgs, ExportImplArgs, ExportStructArgs, ExportTraitArgs, ExportedImplFnAttributes,
};
use crate::util::extract_docstring;
use uniffi_meta::{TraitKind, UniffiTraitDiscriminants};

/// Collect and sort traits deterministically.
/// HashSet iteration order is random per process; sorting ensures reproducible builds.
fn sorted_traits(args: ExportStructArgs) -> Vec<UniffiTraitDiscriminants> {
    let mut traits: Vec<UniffiTraitDiscriminants> = args.traits.into_iter().collect();
    traits.sort_by_key(|t| *t as isize);
    traits
}

pub(super) enum ExportItem {
    Function {
        sig: FnSignature,
        args: ExportFnArgs,
    },
    Impl {
        self_ident: Ident,
        items: Vec<ImplItem>,
        args: ExportImplArgs,
        trait_: Option<syn::Path>,
    },
    Trait {
        self_ident: Ident,
        items: Vec<ImplItem>,
        trait_kind: TraitKind,
        callback_interface_only: bool,
        docstring: String,
        args: ExportTraitArgs,
    },
    Struct {
        self_ident: Ident,
        uniffi_traits: Vec<UniffiTraitDiscriminants>,
    },
    Enum {
        self_ident: Ident,
        uniffi_traits: Vec<UniffiTraitDiscriminants>,
    },
}

impl ExportItem {
    pub fn new(item: syn::Item, attr_args: TokenStream) -> syn::Result<Self> {
        match item {
            syn::Item::Fn(item) => {
                let args: ExportFnArgs = syn::parse(attr_args)?;
                let docstring = extract_docstring(&item.attrs)?;
                let sig = FnSignature::new_function(item.sig, args.clone(), docstring)?;
                Ok(Self::Function { sig, args })
            }
            syn::Item::Impl(item) => Self::from_impl(item, attr_args),
            syn::Item::Trait(item) => Self::from_trait(item, attr_args),
            syn::Item::Struct(item) => Self::from_struct(item, attr_args),
            syn::Item::Enum(item) => Self::from_enum(item, attr_args),
            // FIXME: Support const / static?
            _ => Err(syn::Error::new(
                Span::call_site(),
                "unsupported item: This block doesn't support `uniffi::export`",
            )),
        }
    }

    pub fn from_impl(item: syn::ItemImpl, attr_args: TokenStream) -> syn::Result<Self> {
        let args: ExportImplArgs = syn::parse(attr_args)?;
        if !item.generics.params.is_empty() || item.generics.where_clause.is_some() {
            return Err(syn::Error::new_spanned(
                &item.generics,
                "generic impls are not currently supported by uniffi::export",
            ));
        }

        let type_path = type_as_type_path(&item.self_ty)?;

        if type_path.qself.is_some() {
            return Err(syn::Error::new_spanned(
                type_path,
                "qualified self types are not currently supported by uniffi::export",
            ));
        }

        let self_ident = match type_path.path.get_ident() {
            Some(id) => id,
            None => {
                return Err(syn::Error::new_spanned(
                    type_path,
                    "qualified paths in self-types are not currently supported by uniffi::export",
                ));
            }
        };

        let items = item
            .items
            .into_iter()
            .map(|item| {
                let impl_fn = match item {
                    syn::ImplItem::Fn(m) => m,
                    _ => {
                        return Err(syn::Error::new_spanned(
                            item,
                            "only fn's are supported in impl blocks annotated with uniffi::export",
                        ));
                    }
                };

                let docstring = extract_docstring(&impl_fn.attrs)?;
                let attrs = ExportedImplFnAttributes::new(&impl_fn.attrs)?;

                let foreign_self_ident = if let Some(name) = &args.name {
                    Ident::new(name, self_ident.span())
                } else {
                    self_ident.clone()
                };

                let item = if attrs.constructor {
                    ImplItem::Constructor(FnSignature::new_constructor(
                        self_ident.clone(),
                        foreign_self_ident,
                        impl_fn.sig,
                        attrs.args,
                        docstring,
                    )?)
                } else {
                    ImplItem::Method(FnSignature::new_method(
                        self_ident.clone(),
                        foreign_self_ident,
                        impl_fn.sig,
                        attrs.args,
                        docstring,
                    )?)
                };

                Ok(item)
            })
            .collect::<syn::Result<_>>()?;

        Ok(Self::Impl {
            items,
            self_ident: self_ident.to_owned(),
            args,
            trait_: item.trait_.map(|t| t.1),
        })
    }

    fn from_trait(item: syn::ItemTrait, attr_args: TokenStream) -> syn::Result<Self> {
        let args: ExportTraitArgs = syn::parse(attr_args)?;
        let (trait_kind, callback_interface_only) = if args.rust.is_some() || args.foreign.is_some()
        {
            // Canonical bare-flag syntax: `rust`, `foreign`, or `rust, foreign`
            let kind = match (args.rust.is_some(), args.foreign.is_some()) {
                (true, false) => TraitKind::RustOnly,
                (false, true) => TraitKind::ForeignOnly,
                (true, true) => TraitKind::Both,
                (false, false) => unreachable!(),
            };
            (kind, false)
        } else {
            // Legacy flag-style syntax (kept for backward compatibility)
            let cb_only = args.callback_interface.is_some();
            let kind = if args.with_foreign.is_some() || cb_only {
                TraitKind::Both
            } else {
                TraitKind::RustOnly
            };
            (kind, cb_only)
        };

        if !item.generics.params.is_empty() || item.generics.where_clause.is_some() {
            return Err(syn::Error::new_spanned(
                &item.generics,
                "generic impls are not currently supported by uniffi::export",
            ));
        }

        let self_ident = item.ident.to_owned();
        let docstring = extract_docstring(&item.attrs)?;
        let items = item
            .items
            .into_iter()
            .enumerate()
            .map(|(i, item)| {
                let tim = match item {
                    syn::TraitItem::Fn(tim) => tim,
                    _ => {
                        return Err(syn::Error::new_spanned(
                            item,
                            "only fn's are supported in traits annotated with uniffi::export",
                        ));
                    }
                };
                if let Some(default) = tim.default {
                    return Err(syn::Error::new_spanned(
                        default,
                        "uniffi::export'd trait methods can't have a default implementation.",
                    ));
                }

                let docstring = extract_docstring(&tim.attrs)?;
                let attrs = ExportedImplFnAttributes::new(&tim.attrs)?;
                let item = if attrs.constructor {
                    return Err(syn::Error::new_spanned(
                        tim,
                        "exported traits can not have constructors",
                    ));
                } else {
                    ImplItem::Method(FnSignature::new_trait_method(
                        self_ident.clone(),
                        tim.sig,
                        attrs.args,
                        i as u32,
                        docstring,
                    )?)
                };

                Ok(item)
            })
            .collect::<syn::Result<_>>()?;

        Ok(Self::Trait {
            items,
            self_ident,
            trait_kind,
            callback_interface_only,
            docstring,
            args,
        })
    }

    fn from_struct(item: syn::ItemStruct, attr_args: TokenStream) -> syn::Result<Self> {
        let args: ExportStructArgs = syn::parse(attr_args)?;
        let uniffi_traits = sorted_traits(args);
        if uniffi_traits.is_empty() {
            Err(syn::Error::new(Span::call_site(),
                "uniffi::export on a struct must supply a builtin trait name. Did you mean `#[derive(uniffi::Object)]`?"
            ))
        } else {
            Ok(Self::Struct {
                self_ident: item.ident,
                uniffi_traits,
            })
        }
    }

    fn from_enum(item: syn::ItemEnum, attr_args: TokenStream) -> syn::Result<Self> {
        let args: ExportStructArgs = syn::parse(attr_args)?;
        let uniffi_traits = sorted_traits(args);
        if uniffi_traits.is_empty() {
            Err(syn::Error::new(Span::call_site(),
                "uniffi::export on an enum must supply a builtin trait name. Did you mean `#[derive(uniffi::Enum)]`?"
            ))
        } else {
            Ok(Self::Enum {
                self_ident: item.ident,
                uniffi_traits,
            })
        }
    }
}

pub(super) enum ImplItem {
    Constructor(FnSignature),
    Method(FnSignature),
}

fn type_as_type_path(ty: &syn::Type) -> syn::Result<&syn::TypePath> {
    match ty {
        syn::Type::Group(g) => type_as_type_path(&g.elem),
        syn::Type::Paren(p) => type_as_type_path(&p.elem),
        syn::Type::Path(p) => Ok(p),
        _ => Err(type_not_supported(ty)),
    }
}

fn type_not_supported(ty: &impl ToTokens) -> syn::Error {
    syn::Error::new_spanned(
        ty,
        "this type is not currently supported by uniffi::export in this position",
    )
}