diff --git a/docs/manual/src/proc_macro/index.md b/docs/manual/src/proc_macro/index.md index aad360e660..24d3250c43 100644 --- a/docs/manual/src/proc_macro/index.md +++ b/docs/manual/src/proc_macro/index.md @@ -104,6 +104,12 @@ will fail). pub struct MyRecord { pub field_a: String, pub field_b: Option>, + // Fields can have a default value. + // Currently, only string, integer, float and boolean literals are supported as defaults. + #[uniffi(default = "hello")] + pub greeting: String, + #[uniffi(default = true)] + pub some_flag: bool, } ``` diff --git a/fixtures/metadata/src/tests.rs b/fixtures/metadata/src/tests.rs index b7e5c85c2f..0a3f7d49d2 100644 --- a/fixtures/metadata/src/tests.rs +++ b/fixtures/metadata/src/tests.rs @@ -11,6 +11,7 @@ use uniffi_meta::*; mod person { #[derive(uniffi::Record, Debug)] pub struct Person { + #[uniffi(default = "test")] name: String, age: u16, } @@ -173,10 +174,14 @@ mod test_metadata { FieldMetadata { name: "name".into(), ty: Type::String, + default: Some(Literal::Str { + value: "test".to_owned(), + }), }, FieldMetadata { name: "age".into(), ty: Type::U16, + default: None, }, ], }, @@ -225,6 +230,7 @@ mod test_metadata { fields: vec![FieldMetadata { name: "data".into(), ty: Type::String, + default: None, }], }, VariantMetadata { @@ -234,6 +240,7 @@ mod test_metadata { ty: Type::Record { name: "Person".into(), }, + default: None, }], }, ], @@ -281,6 +288,7 @@ mod test_metadata { fields: vec![FieldMetadata { name: "reason".into(), ty: Type::String, + default: None, }], }, VariantMetadata { @@ -290,6 +298,7 @@ mod test_metadata { ty: Type::Enum { name: "Weapon".into(), }, + default: None, }], }, ], diff --git a/uniffi_bindgen/src/interface/enum_.rs b/uniffi_bindgen/src/interface/enum_.rs index c72f75d1a0..499f28f94a 100644 --- a/uniffi_bindgen/src/interface/enum_.rs +++ b/uniffi_bindgen/src/interface/enum_.rs @@ -120,14 +120,20 @@ impl Enum { } } -impl From for Enum { - fn from(meta: uniffi_meta::EnumMetadata) -> Self { +impl TryFrom for Enum { + type Error = anyhow::Error; + + fn try_from(meta: uniffi_meta::EnumMetadata) -> Result { let flat = meta.variants.iter().all(|v| v.fields.is_empty()); - Self { + Ok(Self { name: meta.name, - variants: meta.variants.into_iter().map(Into::into).collect(), + variants: meta + .variants + .into_iter() + .map(TryInto::try_into) + .collect::>()?, flat, - } + }) } } @@ -210,12 +216,18 @@ impl Variant { } } -impl From for Variant { - fn from(meta: uniffi_meta::VariantMetadata) -> Self { - Self { +impl TryFrom for Variant { + type Error = anyhow::Error; + + fn try_from(meta: uniffi_meta::VariantMetadata) -> Result { + Ok(Self { name: meta.name, - fields: meta.fields.into_iter().map(Into::into).collect(), - } + fields: meta + .fields + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + }) } } diff --git a/uniffi_bindgen/src/interface/error.rs b/uniffi_bindgen/src/interface/error.rs index 9c83aae5fb..26c695a659 100644 --- a/uniffi_bindgen/src/interface/error.rs +++ b/uniffi_bindgen/src/interface/error.rs @@ -135,16 +135,22 @@ impl Error { } } -impl From for Error { - fn from(meta: uniffi_meta::ErrorMetadata) -> Self { - Self { +impl TryFrom for Error { + type Error = anyhow::Error; + + fn try_from(meta: uniffi_meta::ErrorMetadata) -> Result { + Ok(Self { name: meta.name.clone(), enum_: Enum { name: meta.name, - variants: meta.variants.into_iter().map(Into::into).collect(), + variants: meta + .variants + .into_iter() + .map(TryInto::try_into) + .collect::>()?, flat: meta.flat, }, - } + }) } } diff --git a/uniffi_bindgen/src/interface/literal.rs b/uniffi_bindgen/src/interface/literal.rs index da02613684..9a2ce91230 100644 --- a/uniffi_bindgen/src/interface/literal.rs +++ b/uniffi_bindgen/src/interface/literal.rs @@ -7,7 +7,7 @@ //! This module provides support for interpreting literal values from the UDL, //! which appear in places such as default arguments. -use anyhow::{bail, Result}; +use anyhow::{bail, ensure, Context, Result}; use uniffi_meta::Checksum; use super::types::Type; @@ -34,6 +34,66 @@ pub enum Literal { Null, } +impl Literal { + pub(crate) fn from_metadata( + name: &str, + ty: &Type, + default: uniffi_meta::Literal, + ) -> Result { + Ok(match default { + uniffi_meta::Literal::Str { value } => { + ensure!( + matches!(ty, Type::String), + "field {name} of type {ty:?} can't have a default value of type string" + ); + Self::String(value) + } + uniffi_meta::Literal::Int { base10_digits } => { + macro_rules! parse_int { + ($ty:ident, $variant:ident) => { + Self::$variant( + base10_digits + .parse::<$ty>() + .with_context(|| format!("parsing default for field {}", name))? + .into(), + Radix::Decimal, + ty.to_owned(), + ) + }; + } + + match ty { + Type::UInt8 => parse_int!(u8, UInt), + Type::Int8 => parse_int!(i8, Int), + Type::UInt16 => parse_int!(u16, UInt), + Type::Int16 => parse_int!(i16, Int), + Type::UInt32 => parse_int!(u32, UInt), + Type::Int32 => parse_int!(i32, Int), + Type::UInt64 => parse_int!(u64, UInt), + Type::Int64 => parse_int!(i64, Int), + _ => { + bail!("field {name} of type {ty:?} can't have a default value of type integer"); + } + } + } + uniffi_meta::Literal::Float { base10_digits } => match ty { + Type::Float32 => Self::Float(base10_digits, Type::Float32), + Type::Float64 => Self::Float(base10_digits, Type::Float64), + _ => { + bail!("field {name} of type {ty:?} can't have a default value of type float"); + } + }, + uniffi_meta::Literal::Bool { value } => { + ensure!( + matches!(ty, Type::String), + "field {name} of type {ty:?} can't have a default value of type boolean" + ); + Self::Boolean(value) + } + }) + } +} + // Represent the radix of integer literal values. // We preserve the radix into the generated bindings for readability reasons. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Checksum)] diff --git a/uniffi_bindgen/src/interface/record.rs b/uniffi_bindgen/src/interface/record.rs index b000510884..a3b75871ad 100644 --- a/uniffi_bindgen/src/interface/record.rs +++ b/uniffi_bindgen/src/interface/record.rs @@ -85,12 +85,18 @@ impl Record { } } -impl From for Record { - fn from(meta: uniffi_meta::RecordMetadata) -> Self { - Self { +impl TryFrom for Record { + type Error = anyhow::Error; + + fn try_from(meta: uniffi_meta::RecordMetadata) -> Result { + Ok(Self { name: meta.name, - fields: meta.fields.into_iter().map(Into::into).collect(), - } + fields: meta + .fields + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + }) } } @@ -135,13 +141,21 @@ impl Field { } } -impl From for Field { - fn from(meta: uniffi_meta::FieldMetadata) -> Self { - Self { - name: meta.name, - type_: convert_type(&meta.ty), - default: None, - } +impl TryFrom for Field { + type Error = anyhow::Error; + + fn try_from(meta: uniffi_meta::FieldMetadata) -> Result { + let name = meta.name; + let type_ = convert_type(&meta.ty); + let default = meta + .default + .map(|d| Literal::from_metadata(&name, &type_, d)) + .transpose()?; + Ok(Self { + name, + type_, + default, + }) } } diff --git a/uniffi_bindgen/src/macro_metadata/ci.rs b/uniffi_bindgen/src/macro_metadata/ci.rs index ba8f888b02..306bcd9ab5 100644 --- a/uniffi_bindgen/src/macro_metadata/ci.rs +++ b/uniffi_bindgen/src/macro_metadata/ci.rs @@ -76,7 +76,7 @@ pub fn add_to_ci( iface.types.add_known_type(&ty)?; iface.types.add_type_definition(&meta.name, ty)?; - let record: Record = meta.into(); + let record: Record = meta.try_into()?; iface.add_record_definition(record)?; } Metadata::Enum(meta) => { @@ -84,7 +84,7 @@ pub fn add_to_ci( iface.types.add_known_type(&ty)?; iface.types.add_type_definition(&meta.name, ty)?; - let enum_: Enum = meta.into(); + let enum_: Enum = meta.try_into()?; iface.add_enum_definition(enum_)?; } Metadata::Object(meta) => { @@ -95,7 +95,7 @@ pub fn add_to_ci( iface.types.add_known_type(&ty)?; iface.types.add_type_definition(&meta.name, ty)?; - let error: Error = meta.into(); + let error: Error = meta.try_into()?; iface.add_error_definition(error)?; } } diff --git a/uniffi_core/src/metadata.rs b/uniffi_core/src/metadata.rs index cd1d4474d7..0cee981cb9 100644 --- a/uniffi_core/src/metadata.rs +++ b/uniffi_core/src/metadata.rs @@ -65,6 +65,12 @@ pub mod codes { pub const TYPE_FUTURE: u8 = 24; pub const TYPE_FOREIGN_EXECUTOR: u8 = 25; pub const TYPE_UNIT: u8 = 255; + + // Literal codes + pub const LIT_STR: u8 = 0; + pub const LIT_INT: u8 = 1; + pub const LIT_FLOAT: u8 = 2; + pub const LIT_BOOL: u8 = 3; } const BUF_SIZE: usize = 2048; diff --git a/uniffi_macros/src/enum_.rs b/uniffi_macros/src/enum_.rs index ac3f4b8b44..19c836c4b0 100644 --- a/uniffi_macros/src/enum_.rs +++ b/uniffi_macros/src/enum_.rs @@ -194,6 +194,8 @@ pub fn variant_metadata(enum_: &DataEnum) -> syn::Result> { #( .concat_str(#field_names) .concat(<#field_types as ::uniffi::FfiConverter>::TYPE_ID_META) + // field defaults not yet supported for enums + .concat_bool(false) )* }) }) diff --git a/uniffi_macros/src/lib.rs b/uniffi_macros/src/lib.rs index 4970ae2398..0c4a052310 100644 --- a/uniffi_macros/src/lib.rs +++ b/uniffi_macros/src/lib.rs @@ -65,7 +65,7 @@ pub fn export(attr_args: TokenStream, input: TokenStream) -> TokenStream { .into() } -#[proc_macro_derive(Record)] +#[proc_macro_derive(Record, attributes(uniffi))] pub fn derive_record(input: TokenStream) -> TokenStream { expand_record(parse_macro_input!(input)).into() } diff --git a/uniffi_macros/src/record.rs b/uniffi_macros/src/record.rs index 8829b8f110..2c80a1b2a0 100644 --- a/uniffi_macros/src/record.rs +++ b/uniffi_macros/src/record.rs @@ -1,11 +1,11 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use syn::{Data, DataStruct, DeriveInput, Field, Path}; +use syn::{parse::ParseStream, Data, DataStruct, DeriveInput, Field, Lit, Path, Token}; use crate::util::{ - create_metadata_items, ident_to_string, mod_path, tagged_impl_header, + create_metadata_items, either_attribute_arg, ident_to_string, mod_path, tagged_impl_header, try_metadata_value_from_usize, try_read_field, ArgumentNotAllowedHere, AttributeSliceExt, - CommonAttr, + CommonAttr, UniffiAttributeArgs, }; pub fn expand_record(input: DeriveInput) -> TokenStream { @@ -87,6 +87,32 @@ fn write_field(f: &Field) -> TokenStream { } } +mod kw { + syn::custom_keyword!(default); +} + +#[derive(Default)] +pub struct FieldAttributeArguments { + pub(crate) default: Option, +} + +impl UniffiAttributeArgs for FieldAttributeArguments { + fn parse_one(input: ParseStream<'_>) -> syn::Result { + let _: kw::default = input.parse()?; + let _: Token![=] = input.parse()?; + let default = input.parse()?; + Ok(Self { + default: Some(default), + }) + } + + fn merge(self, other: Self) -> syn::Result { + Ok(Self { + default: either_attribute_arg(self.default, other.default)?, + }) + } +} + pub(crate) fn record_meta_static_var( ident: &Ident, record: &DataStruct, @@ -96,14 +122,34 @@ pub(crate) fn record_meta_static_var( let fields_len = try_metadata_value_from_usize(record.fields.len(), "UniFFI limits structs to 256 fields")?; - let concat_fields = record.fields.iter().map(|f| { - let name = ident_to_string(f.ident.as_ref().unwrap()); - let ty = &f.ty; - quote! { - .concat_str(#name) - .concat(<#ty as ::uniffi::FfiConverter>::TYPE_ID_META) - } - }); + let concat_fields: TokenStream = record + .fields + .iter() + .map(|f| { + let attrs = f + .attrs + .parse_uniffi_attr_args::()?; + + let name = ident_to_string(f.ident.as_ref().unwrap()); + let ty = &f.ty; + let default = match attrs.default { + Some(default) => { + let default_value = default_value_concat_calls(default)?; + quote! { + .concat_bool(true) + #default_value + } + } + None => quote! { .concat_bool(false) }, + }; + + Ok(quote! { + .concat_str(#name) + .concat(<#ty as ::uniffi::FfiConverter>::TYPE_ID_META) + #default + }) + }) + .collect::>()?; Ok(create_metadata_items( "record", @@ -113,8 +159,49 @@ pub(crate) fn record_meta_static_var( .concat_str(#module_path) .concat_str(#name) .concat_value(#fields_len) - #(#concat_fields)* + #concat_fields }, None, )) } + +fn default_value_concat_calls(default: Lit) -> syn::Result { + match default { + Lit::Int(i) if !i.suffix().is_empty() => Err(syn::Error::new_spanned( + i, + "integer literals with suffix not supported here", + )), + Lit::Float(f) if !f.suffix().is_empty() => Err(syn::Error::new_spanned( + f, + "float literals with suffix not supported here", + )), + + Lit::Str(s) => Ok(quote! { + .concat_value(::uniffi::metadata::codes::LIT_STR) + .concat_str(#s) + }), + Lit::Int(i) => { + let digits = i.base10_digits(); + Ok(quote! { + .concat_value(::uniffi::metadata::codes::LIT_INT) + .concat_str(#digits) + }) + } + Lit::Float(f) => { + let digits = f.base10_digits(); + Ok(quote! { + .concat_value(::uniffi::metadata::codes::LIT_FLOAT) + .concat_str(#digits) + }) + } + Lit::Bool(b) => Ok(quote! { + .concat_value(::uniffi::metadata::codes::LIT_BOOL) + .concat_bool(#b) + }), + + _ => Err(syn::Error::new_spanned( + default, + "this type of literal is not currently supported as a default", + )), + } +} diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index 6668cc6e22..6b76d0afcd 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -230,6 +230,14 @@ pub enum Type { }, } +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub enum Literal { + Str { value: String }, + Int { base10_digits: String }, + Float { base10_digits: String }, + Bool { value: bool }, +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct RecordMetadata { pub module_path: String, @@ -242,6 +250,7 @@ pub struct FieldMetadata { pub name: String, #[serde(rename = "type")] pub ty: Type, + pub default: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] diff --git a/uniffi_meta/src/reader.rs b/uniffi_meta/src/reader.rs index 2d176fca03..6f9f5dbfed 100644 --- a/uniffi_meta/src/reader.rs +++ b/uniffi_meta/src/reader.rs @@ -268,10 +268,10 @@ impl<'a> MetadataReader<'a> { let len = self.read_u8()?; (0..len) .map(|_| { - Ok(FieldMetadata { - name: self.read_string()?, - ty: self.read_type()?, - }) + let name = self.read_string()?; + let ty = self.read_type()?; + let default = self.read_default()?; + Ok(FieldMetadata { name, ty, default }) }) .collect() } @@ -317,4 +317,28 @@ impl<'a> MetadataReader<'a> { let metadata_buf = &self.initial_data[..bytes_read]; checksum_metadata(metadata_buf) } + + fn read_default(&mut self) -> Result> { + let has_default = self.read_bool()?; + if !has_default { + return Ok(None); + } + + let literal_kind = self.read_u8()?; + Ok(Some(match literal_kind { + codes::LIT_STR => Literal::Str { + value: self.read_string()?, + }, + codes::LIT_INT => Literal::Int { + base10_digits: self.read_string()?, + }, + codes::LIT_FLOAT => Literal::Float { + base10_digits: self.read_string()?, + }, + codes::LIT_BOOL => Literal::Bool { + value: self.read_bool()?, + }, + _ => bail!("Unexpected literal kind code: {literal_kind:?}"), + })) + } }