use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::{ parse::{Parse, ParseStream}, spanned::Spanned, Attribute, Data, DataEnum, DeriveInput, Expr, Index, Lit, Variant, }; use crate::util::{ create_metadata_items, derive_all_ffi_traits, either_attribute_arg, extract_docstring, ident_to_string, kw, mod_path, parse_comma_separated, tagged_impl_header, try_metadata_value_from_usize, try_read_field, AttributeSliceExt, UniffiAttributeArgs, }; fn extract_repr(attrs: &[Attribute]) -> syn::Result> { let mut result = None; for attr in attrs { if attr.path().is_ident("repr") { attr.parse_nested_meta(|meta| { result = match meta.path.get_ident() { Some(i) => { let s = i.to_string(); match s.as_str() { "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" | "isize" => Some(i.clone()), // while the default repr for an enum is `isize` we don't apply that default here. _ => None, } } _ => None, }; Ok(()) })? } } Ok(result) } pub fn expand_enum( input: DeriveInput, // Attributes from #[derive_error_for_udl()], if we are in udl mode attr_from_udl_mode: Option, udl_mode: bool, ) -> syn::Result { let enum_ = match input.data { Data::Enum(e) => e, _ => { return Err(syn::Error::new( Span::call_site(), "This derive must only be used on enums", )) } }; let ident = &input.ident; let docstring = extract_docstring(&input.attrs)?; let discr_type = extract_repr(&input.attrs)?; let mut attr: EnumAttr = input.attrs.parse_uniffi_attr_args()?; if let Some(attr_from_udl_mode) = attr_from_udl_mode { attr = attr.merge(attr_from_udl_mode)?; } let ffi_converter_impl = enum_ffi_converter_impl(ident, &enum_, udl_mode, &attr); let meta_static_var = (!udl_mode).then(|| { enum_meta_static_var(ident, docstring, discr_type, &enum_, &attr) .unwrap_or_else(syn::Error::into_compile_error) }); Ok(quote! { #ffi_converter_impl #meta_static_var }) } pub(crate) fn enum_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, attr: &EnumAttr, ) -> TokenStream { enum_or_error_ffi_converter_impl( ident, enum_, udl_mode, attr, quote! { ::uniffi::metadata::codes::TYPE_ENUM }, ) } pub(crate) fn rich_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, attr: &EnumAttr, ) -> TokenStream { enum_or_error_ffi_converter_impl( ident, enum_, udl_mode, attr, quote! { ::uniffi::metadata::codes::TYPE_ENUM }, ) } fn enum_or_error_ffi_converter_impl( ident: &Ident, enum_: &DataEnum, udl_mode: bool, attr: &EnumAttr, metadata_type_code: TokenStream, ) -> TokenStream { let name = ident_to_string(ident); let impl_spec = tagged_impl_header("FfiConverter", ident, udl_mode); let derive_ffi_traits = derive_all_ffi_traits(ident, udl_mode); let mod_path = match mod_path() { Ok(p) => p, Err(e) => return e.into_compile_error(), }; let mut write_match_arms: Vec<_> = enum_ .variants .iter() .enumerate() .map(|(i, v)| { let v_ident = &v.ident; let field_idents = v .fields .iter() .enumerate() .map(|(i, f)| { f.ident .clone() .unwrap_or_else(|| Ident::new(&format!("e{i}"), f.span())) }) .collect::>(); let idx = Index::from(i + 1); let write_fields = std::iter::zip(v.fields.iter(), field_idents.iter()).map(|(f, ident)| { let ty = &f.ty; quote! { <#ty as ::uniffi::Lower>::write(#ident, buf); } }); let is_tuple = v.fields.iter().any(|f| f.ident.is_none()); let fields = if is_tuple { quote! { ( #(#field_idents),* ) } } else { quote! { { #(#field_idents),* } } }; quote! { Self::#v_ident #fields => { ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); #(#write_fields)* } } }) .collect(); if attr.non_exhaustive.is_some() { write_match_arms.push(quote! { _ => panic!("Unexpected variant in non-exhaustive enum"), }) } let write_impl = quote! { match obj { #(#write_match_arms)* } }; let try_read_match_arms = enum_.variants.iter().enumerate().map(|(i, v)| { let idx = Index::from(i + 1); let v_ident = &v.ident; let is_tuple = v.fields.iter().any(|f| f.ident.is_none()); let try_read_fields = v.fields.iter().map(try_read_field); if is_tuple { quote! { #idx => Self::#v_ident ( #(#try_read_fields)* ), } } else { quote! { #idx => Self::#v_ident { #(#try_read_fields)* }, } } }); let error_format_string = format!("Invalid {ident} enum value: {{}}"); let try_read_impl = quote! { ::uniffi::check_remaining(buf, 4)?; Ok(match ::uniffi::deps::bytes::Buf::get_i32(buf) { #(#try_read_match_arms)* v => ::uniffi::deps::anyhow::bail!(#error_format_string, v), }) }; quote! { #[automatically_derived] unsafe #impl_spec { ::uniffi::ffi_converter_rust_buffer_lift_and_lower!(crate::UniFfiTag); fn write(obj: Self, buf: &mut ::std::vec::Vec) { #write_impl } fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result { #try_read_impl } const TYPE_ID_META: ::uniffi::MetadataBuffer = ::uniffi::MetadataBuffer::from_code(#metadata_type_code) .concat_str(#mod_path) .concat_str(#name); } #derive_ffi_traits } } pub(crate) fn enum_meta_static_var( ident: &Ident, docstring: String, discr_type: Option, enum_: &DataEnum, attr: &EnumAttr, ) -> syn::Result { let name = ident_to_string(ident); let module_path = mod_path()?; let non_exhaustive = attr.non_exhaustive.is_some(); let mut metadata_expr = quote! { ::uniffi::MetadataBuffer::from_code(::uniffi::metadata::codes::ENUM) .concat_str(#module_path) .concat_str(#name) .concat_option_bool(None) // forced_flatness }; metadata_expr.extend(match discr_type { None => quote! { .concat_bool(false) }, Some(t) => quote! { .concat_bool(true).concat(<#t as ::uniffi::Lower>::TYPE_ID_META) } }); metadata_expr.extend(variant_metadata(enum_)?); metadata_expr.extend(quote! { .concat_bool(#non_exhaustive) .concat_long_str(#docstring) }); Ok(create_metadata_items("enum", &name, metadata_expr, None)) } fn variant_value(v: &Variant) -> syn::Result { let Some((_, e)) = &v.discriminant else { return Ok(quote! { .concat_bool(false) }); }; // Attempting to expose an enum value which we don't understand is a hard-error // rather than silently ignoring it. If we had the ability to emit a warning that // might make more sense. // We can't sanely handle most expressions other than literals, but we can handle // negative literals. let mut negate = false; let lit = match e { Expr::Lit(lit) => lit, Expr::Unary(expr_unary) if matches!(expr_unary.op, syn::UnOp::Neg(_)) => { negate = true; match *expr_unary.expr { Expr::Lit(ref lit) => lit, _ => { return Err(syn::Error::new_spanned( e, "UniFFI disciminant values must be a literal", )); } } } _ => { return Err(syn::Error::new_spanned( e, "UniFFI disciminant values must be a literal", )); } }; let Lit::Int(ref intlit) = lit.lit else { return Err(syn::Error::new_spanned( v, "UniFFI disciminant values must be a literal integer", )); }; if !intlit.suffix().is_empty() { return Err(syn::Error::new_spanned( intlit, "integer literals with suffix not supported by UniFFI here", )); } let digits = if negate { format!("-{}", intlit.base10_digits()) } else { intlit.base10_digits().to_string() }; Ok(quote! { .concat_bool(true) .concat_value(::uniffi::metadata::codes::LIT_INT) .concat_str(#digits) }) } pub fn variant_metadata(enum_: &DataEnum) -> syn::Result> { let variants_len = try_metadata_value_from_usize(enum_.variants.len(), "UniFFI limits enums to 256 variants")?; std::iter::once(Ok(quote! { .concat_value(#variants_len) })) .chain(enum_.variants.iter().map(|v| { let fields_len = try_metadata_value_from_usize( v.fields.len(), "UniFFI limits enum variants to 256 fields", )?; let field_names = v .fields .iter() .map(|f| f.ident.as_ref().map(ident_to_string).unwrap_or_default()) .collect::>(); let name = ident_to_string(&v.ident); let value_tokens = variant_value(v)?; let docstring = extract_docstring(&v.attrs)?; let field_types = v.fields.iter().map(|f| &f.ty); let field_docstrings = v .fields .iter() .map(|f| extract_docstring(&f.attrs)) .collect::>>()?; Ok(quote! { .concat_str(#name) #value_tokens .concat_value(#fields_len) #( .concat_str(#field_names) .concat(<#field_types as ::uniffi::Lower>::TYPE_ID_META) // field defaults not yet supported for enums .concat_bool(false) .concat_long_str(#field_docstrings) )* .concat_long_str(#docstring) }) })) .collect() } #[derive(Default)] pub struct EnumAttr { pub non_exhaustive: Option, } // So ErrorAttr can be used with `parse_macro_input!` impl Parse for EnumAttr { fn parse(input: ParseStream<'_>) -> syn::Result { parse_comma_separated(input) } } impl UniffiAttributeArgs for EnumAttr { fn parse_one(input: ParseStream<'_>) -> syn::Result { let lookahead = input.lookahead1(); if lookahead.peek(kw::non_exhaustive) { Ok(Self { non_exhaustive: input.parse()?, }) } else { Err(lookahead.error()) } } fn merge(self, other: Self) -> syn::Result { Ok(Self { non_exhaustive: either_attribute_arg(self.non_exhaustive, other.non_exhaustive)?, }) } }