diff options
Diffstat (limited to '')
-rw-r--r-- | third_party/rust/uniffi_macros/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/Cargo.toml | 69 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/enum_.rs | 160 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/error.rs | 170 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export.rs | 210 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export/metadata.rs | 26 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export/metadata/convert.rs | 222 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export/metadata/function.rs | 32 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export/metadata/impl_.rs | 93 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export/scaffolding.rs | 199 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/lib.rs | 179 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/object.rs | 35 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/record.rs | 108 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/test.rs | 90 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/util.rs | 223 |
15 files changed, 1817 insertions, 0 deletions
diff --git a/third_party/rust/uniffi_macros/.cargo-checksum.json b/third_party/rust/uniffi_macros/.cargo-checksum.json new file mode 100644 index 0000000000..8a84c40226 --- /dev/null +++ b/third_party/rust/uniffi_macros/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"495a97879e9a66a87a410c742fa2ac36e2b85e5f16bee1cdde878db02090d7ee","src/enum_.rs":"0b2aa1bce2b8f1c476f2874e6bb9ae49b9610abd6963f3336db63794296494a8","src/error.rs":"b5fccfa3d4aa2d7454c5d27504a2d8d2e02e761843ea2da94b76ef8ffb0d0bf5","src/export.rs":"f95d31c9445142efe59617392ce9135acbefc4ac852654a482a2360c5c78ee75","src/export/metadata.rs":"af89a9942c7c0c4043a3cd57d1e6bd71cde19005e1f9f246efac761f47eff6be","src/export/metadata/convert.rs":"82c9602ddee0af2c93733bbe6cb77c50dccb0134c927b582f04f5bb9f8b5203c","src/export/metadata/function.rs":"e775f838025bee429b294f786579ac6598966b1b4f1f415dfb9012c70a0fea6c","src/export/metadata/impl_.rs":"1f55990f15293f2a3e30ebb0c132f68caa25a56113e0451c5307c9ed32cb8fb8","src/export/scaffolding.rs":"1cd5b8cf62195e35cd851ee9dd5aa787eaa8a1c293c1b3e899a1de8feb183b2d","src/lib.rs":"4357a13d0e9deac75239007431211188d7ef6270256f002d17d9e937ee3aba5a","src/object.rs":"955b596f344304013692042bdc1760bbb1192ec33950b0dd2932cb8de94ec297","src/record.rs":"f0c2adb794d5996ff8545c10469e63f24164509f0566e6c401a26c363c278003","src/test.rs":"069d91947685de083c9faa9042c328c235445b84bcf69f03117016d9c8464736","src/util.rs":"4834ece6808595ef5fbf735f9610dd1ef1631ea2e5919e6a1da9f290beb7091a"},"package":"fa03394de21e759e0022f1ea8d992d2e39290d735b9ed52b1f74b20a684f794e"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_macros/Cargo.toml b/third_party/rust/uniffi_macros/Cargo.toml new file mode 100644 index 0000000000..dc5b9a9184 --- /dev/null +++ b/third_party/rust/uniffi_macros/Cargo.toml @@ -0,0 +1,69 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_macros" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust (convenience macros)" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +keywords = [ + "ffi", + "bindgen", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[lib] +proc-macro = true + +[dependencies.bincode] +version = "1.3" + +[dependencies.camino] +version = "1.0.8" + +[dependencies.fs-err] +version = "2.7.0" + +[dependencies.once_cell] +version = "1.10.0" + +[dependencies.proc-macro2] +version = "1.0" + +[dependencies.quote] +version = "1.0" + +[dependencies.serde] +version = "1.0.136" + +[dependencies.syn] +version = "1.0" +features = [ + "full", + "visit-mut", +] + +[dependencies.toml] +version = "0.5.9" + +[dependencies.uniffi_build] +version = "=0.23.0" + +[dependencies.uniffi_meta] +version = "=0.23.0" + +[features] +default = [] +nightly = [] diff --git a/third_party/rust/uniffi_macros/src/enum_.rs b/third_party/rust/uniffi_macros/src/enum_.rs new file mode 100644 index 0000000000..c4e49beb8b --- /dev/null +++ b/third_party/rust/uniffi_macros/src/enum_.rs @@ -0,0 +1,160 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{punctuated::Punctuated, Data, DeriveInput, Field, Index, Token, Variant}; +use uniffi_meta::{EnumMetadata, FieldMetadata, VariantMetadata}; + +use crate::{ + export::metadata::convert::convert_type, + util::{assert_type_eq, create_metadata_static_var, try_read_field}, +}; + +pub fn expand_enum(input: DeriveInput, module_path: Vec<String>) -> TokenStream { + let variants = match input.data { + Data::Enum(e) => Some(e.variants), + _ => None, + }; + + let ident = &input.ident; + + let ffi_converter_impl = enum_ffi_converter_impl(variants.as_ref(), ident); + + let meta_static_var = if let Some(variants) = variants { + match enum_metadata(ident, variants, module_path) { + Ok(metadata) => create_metadata_static_var(ident, metadata.into()), + Err(e) => e.into_compile_error(), + } + } else { + syn::Error::new(Span::call_site(), "This derive must only be used on enums") + .into_compile_error() + }; + + let type_assertion = assert_type_eq(ident, quote! { crate::uniffi_types::#ident }); + + quote! { + #ffi_converter_impl + #meta_static_var + #type_assertion + } +} + +pub(crate) fn enum_ffi_converter_impl( + variants: Option<&Punctuated<Variant, Token![,]>>, + ident: &Ident, +) -> TokenStream { + let (write_impl, try_read_impl) = match variants { + Some(variants) => { + let write_match_arms = variants.iter().enumerate().map(|(i, v)| { + let v_ident = &v.ident; + let fields = v.fields.iter().map(|f| &f.ident); + let idx = Index::from(i + 1); + let write_fields = v.fields.iter().map(write_field); + + quote! { + Self::#v_ident { #(#fields),* } => { + ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); + #(#write_fields)* + } + } + }); + let write_impl = quote! { + match obj { #(#write_match_arms)* } + }; + + let try_read_match_arms = variants.iter().enumerate().map(|(i, v)| { + let idx = Index::from(i + 1); + let v_ident = &v.ident; + let try_read_fields = v.fields.iter().map(try_read_field); + + 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), + }) + }; + + (write_impl, try_read_impl) + } + None => { + let unimplemented = quote! { ::std::unimplemented!() }; + (unimplemented.clone(), unimplemented) + } + }; + + quote! { + #[automatically_derived] + impl ::uniffi::RustBufferFfiConverter for #ident { + type RustType = Self; + + fn write(obj: Self, buf: &mut ::std::vec::Vec<u8>) { + #write_impl + } + + fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result<Self> { + #try_read_impl + } + } + } +} + +fn enum_metadata( + ident: &Ident, + variants: Punctuated<Variant, Token![,]>, + module_path: Vec<String>, +) -> syn::Result<EnumMetadata> { + let name = ident.to_string(); + let variants = variants + .iter() + .map(variant_metadata) + .collect::<syn::Result<_>>()?; + + Ok(EnumMetadata { + module_path, + name, + variants, + }) +} + +pub(crate) fn variant_metadata(v: &Variant) -> syn::Result<VariantMetadata> { + let name = v.ident.to_string(); + let fields = v + .fields + .iter() + .map(|f| field_metadata(f, v)) + .collect::<syn::Result<_>>()?; + + Ok(VariantMetadata { name, fields }) +} + +fn field_metadata(f: &Field, v: &Variant) -> syn::Result<FieldMetadata> { + let name = f + .ident + .as_ref() + .ok_or_else(|| { + syn::Error::new_spanned( + v, + "UniFFI only supports enum variants with named fields (or no fields at all)", + ) + })? + .to_string(); + + Ok(FieldMetadata { + name, + ty: convert_type(&f.ty)?, + }) +} + +fn write_field(f: &Field) -> TokenStream { + let ident = &f.ident; + let ty = &f.ty; + + quote! { + <#ty as ::uniffi::FfiConverter>::write(#ident, buf); + } +} diff --git a/third_party/rust/uniffi_macros/src/error.rs b/third_party/rust/uniffi_macros/src/error.rs new file mode 100644 index 0000000000..76c34cf78d --- /dev/null +++ b/third_party/rust/uniffi_macros/src/error.rs @@ -0,0 +1,170 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Data, DeriveInput, Index, Token, Variant, +}; +use uniffi_meta::{ErrorMetadata, VariantMetadata}; + +use crate::{ + enum_::{enum_ffi_converter_impl, variant_metadata}, + util::{ + assert_type_eq, chain, create_metadata_static_var, either_attribute_arg, AttributeSliceExt, + UniffiAttribute, + }, +}; + +pub fn expand_error(input: DeriveInput, module_path: Vec<String>) -> TokenStream { + let variants = match input.data { + Data::Enum(e) => Ok(e.variants), + _ => Err(syn::Error::new( + Span::call_site(), + "This derive currently only supports enums", + )), + }; + + let ident = &input.ident; + let attr = input.attrs.parse_uniffi_attributes::<ErrorAttr>(); + let ffi_converter_impl = match &attr { + Ok(a) if a.flat.is_some() => flat_error_ffi_converter_impl(variants.as_ref().ok(), ident), + _ => enum_ffi_converter_impl(variants.as_ref().ok(), ident), + }; + + let meta_static_var = match (&variants, &attr) { + (Ok(vs), Ok(a)) => Some(match error_metadata(ident, vs, module_path, a) { + Ok(metadata) => create_metadata_static_var(ident, metadata.into()), + Err(e) => e.into_compile_error(), + }), + _ => None, + }; + + let type_assertion = assert_type_eq(ident, quote! { crate::uniffi_types::#ident }); + let variant_errors: TokenStream = match variants { + Ok(vs) => vs + .iter() + .flat_map(|variant| { + chain( + variant.attrs.attributes_not_allowed_here(), + variant + .fields + .iter() + .flat_map(|field| field.attrs.attributes_not_allowed_here()), + ) + }) + .map(syn::Error::into_compile_error) + .collect(), + Err(e) => e.into_compile_error(), + }; + let attr_error = attr.err().map(syn::Error::into_compile_error); + + quote! { + #ffi_converter_impl + + #[automatically_derived] + impl ::uniffi::FfiError for #ident {} + + #meta_static_var + #type_assertion + #variant_errors + #attr_error + } +} + +pub(crate) fn flat_error_ffi_converter_impl( + variants: Option<&Punctuated<Variant, Token![,]>>, + ident: &Ident, +) -> TokenStream { + let write_impl = match variants { + Some(variants) => { + let write_match_arms = variants.iter().enumerate().map(|(i, v)| { + let v_ident = &v.ident; + let idx = Index::from(i + 1); + + quote! { + Self::#v_ident { .. } => { + ::uniffi::deps::bytes::BufMut::put_i32(buf, #idx); + <::std::string::String as ::uniffi::FfiConverter>::write(error_msg, buf); + } + } + }); + let write_impl = quote! { + let error_msg = ::std::string::ToString::to_string(&obj); + match obj { #(#write_match_arms)* } + }; + + write_impl + } + None => quote! { ::std::unimplemented!() }, + }; + + quote! { + #[automatically_derived] + impl ::uniffi::RustBufferFfiConverter for #ident { + type RustType = Self; + + fn write(obj: Self, buf: &mut ::std::vec::Vec<u8>) { + #write_impl + } + + fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result<Self> { + ::std::panic!("try_read not supported for flat errors"); + } + } + } +} + +fn error_metadata( + ident: &Ident, + variants: &Punctuated<Variant, Token![,]>, + module_path: Vec<String>, + attr: &ErrorAttr, +) -> syn::Result<ErrorMetadata> { + let name = ident.to_string(); + let flat = attr.flat.is_some(); + let variants = if flat { + variants + .iter() + .map(|v| VariantMetadata { + name: v.ident.to_string(), + fields: vec![], + }) + .collect() + } else { + variants + .iter() + .map(variant_metadata) + .collect::<syn::Result<_>>()? + }; + + Ok(ErrorMetadata { + module_path, + name, + variants, + flat, + }) +} + +mod kw { + syn::custom_keyword!(flat_error); +} + +#[derive(Default)] +struct ErrorAttr { + flat: Option<kw::flat_error>, +} + +impl Parse for ErrorAttr { + fn parse(input: ParseStream<'_>) -> syn::Result<Self> { + let flat = input.parse()?; + Ok(ErrorAttr { flat }) + } +} + +impl UniffiAttribute for ErrorAttr { + fn merge(self, other: Self) -> syn::Result<Self> { + Ok(Self { + flat: either_attribute_arg(self.flat, other.flat)?, + }) + } +} diff --git a/third_party/rust/uniffi_macros/src/export.rs b/third_party/rust/uniffi_macros/src/export.rs new file mode 100644 index 0000000000..22d469eec6 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export.rs @@ -0,0 +1,210 @@ +/* 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 std::collections::BTreeMap; + +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use uniffi_meta::{checksum, FnMetadata, MethodMetadata, Type}; + +pub(crate) mod metadata; +mod scaffolding; + +pub use self::metadata::gen_metadata; +use self::{ + metadata::convert::{convert_type, try_split_result}, + scaffolding::{gen_fn_scaffolding, gen_method_scaffolding}, +}; +use crate::util::{assert_type_eq, create_metadata_static_var}; + +// TODO(jplatte): Ensure no generics, no async, … +// TODO(jplatte): Aggregate errors instead of short-circuiting, wherever possible + +pub enum ExportItem { + Function { + sig: Signature, + metadata: FnMetadata, + }, + Impl { + self_ident: Ident, + methods: Vec<syn::Result<Method>>, + }, +} + +pub struct Method { + sig: Signature, + metadata: MethodMetadata, +} + +pub struct Signature { + ident: Ident, + inputs: Vec<syn::FnArg>, + output: Option<FunctionReturn>, +} + +impl Signature { + fn new(item: syn::Signature) -> syn::Result<Self> { + let output = match item.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => Some(FunctionReturn::new(ty)?), + }; + + Ok(Self { + ident: item.ident, + inputs: item.inputs.into_iter().collect(), + output, + }) + } +} + +pub struct FunctionReturn { + ty: Box<syn::Type>, + throws: Option<Ident>, +} + +impl FunctionReturn { + fn new(ty: Box<syn::Type>) -> syn::Result<Self> { + Ok(match try_split_result(&ty)? { + Some((ok_type, throws)) => FunctionReturn { + ty: Box::new(ok_type.to_owned()), + throws: Some(throws), + }, + None => FunctionReturn { ty, throws: None }, + }) + } +} + +pub fn expand_export(metadata: ExportItem, mod_path: &[String]) -> TokenStream { + match metadata { + ExportItem::Function { sig, metadata } => { + let checksum = checksum(&metadata); + let scaffolding = gen_fn_scaffolding(&sig, mod_path, checksum); + let type_assertions = fn_type_assertions(&sig); + let meta_static_var = create_metadata_static_var(&sig.ident, metadata.into()); + + quote! { + #scaffolding + #type_assertions + #meta_static_var + } + } + ExportItem::Impl { + methods, + self_ident, + } => { + let method_tokens: TokenStream = methods + .into_iter() + .map(|res| { + res.map_or_else( + syn::Error::into_compile_error, + |Method { sig, metadata }| { + let checksum = checksum(&metadata); + let scaffolding = + gen_method_scaffolding(&sig, mod_path, checksum, &self_ident); + let type_assertions = fn_type_assertions(&sig); + let meta_static_var = create_metadata_static_var( + &format_ident!("{}_{}", metadata.self_name, sig.ident), + metadata.into(), + ); + + quote! { + #scaffolding + #type_assertions + #meta_static_var + } + }, + ) + }) + .collect(); + + quote_spanned! {self_ident.span()=> + ::uniffi::deps::static_assertions::assert_type_eq_all!( + #self_ident, + crate::uniffi_types::#self_ident + ); + + #method_tokens + } + } + } +} + +fn fn_type_assertions(sig: &Signature) -> TokenStream { + // Convert uniffi_meta::Type back to a Rust type + fn convert_type_back(ty: &Type) -> TokenStream { + match &ty { + Type::U8 => quote! { ::std::primitive::u8 }, + Type::U16 => quote! { ::std::primitive::u16 }, + Type::U32 => quote! { ::std::primitive::u32 }, + Type::U64 => quote! { ::std::primitive::u64 }, + Type::I8 => quote! { ::std::primitive::i8 }, + Type::I16 => quote! { ::std::primitive::i16 }, + Type::I32 => quote! { ::std::primitive::i32 }, + Type::I64 => quote! { ::std::primitive::i64 }, + Type::F32 => quote! { ::std::primitive::f32 }, + Type::F64 => quote! { ::std::primitive::f64 }, + Type::Bool => quote! { ::std::primitive::bool }, + Type::String => quote! { ::std::string::String }, + Type::Option { inner_type } => { + let inner = convert_type_back(inner_type); + quote! { ::std::option::Option<#inner> } + } + Type::Vec { inner_type } => { + let inner = convert_type_back(inner_type); + quote! { ::std::vec::Vec<#inner> } + } + Type::HashMap { + key_type, + value_type, + } => { + let key = convert_type_back(key_type); + let value = convert_type_back(value_type); + quote! { ::std::collections::HashMap<#key, #value> } + } + Type::ArcObject { object_name } => { + let object_ident = format_ident!("{object_name}"); + quote! { ::std::sync::Arc<crate::uniffi_types::#object_ident> } + } + Type::Unresolved { name } => { + let ident = format_ident!("{name}"); + quote! { crate::uniffi_types::#ident } + } + } + } + + let input_types = sig.inputs.iter().filter_map(|input| match input { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(pat_ty) => match &*pat_ty.pat { + // Self type is asserted separately for impl blocks + syn::Pat::Ident(i) if i.ident == "self" => None, + _ => Some(&pat_ty.ty), + }, + }); + let output_type = sig.output.as_ref().map(|s| &s.ty); + + let type_assertions: BTreeMap<_, _> = input_types + .chain(output_type) + .filter_map(|ty| { + convert_type(ty).ok().map(|meta_ty| { + let expected_ty = convert_type_back(&meta_ty); + let assert = assert_type_eq(ty, expected_ty); + (meta_ty, assert) + }) + }) + .collect(); + let input_output_type_assertions: TokenStream = type_assertions.into_values().collect(); + + let throws_type_assertion = sig.output.as_ref().and_then(|s| { + let ident = s.throws.as_ref()?; + Some(assert_type_eq( + ident, + quote! { crate::uniffi_types::#ident }, + )) + }); + + quote! { + #input_output_type_assertions + #throws_type_assertion + } +} diff --git a/third_party/rust/uniffi_macros/src/export/metadata.rs b/third_party/rust/uniffi_macros/src/export/metadata.rs new file mode 100644 index 0000000000..2d0b284333 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata.rs @@ -0,0 +1,26 @@ +/* 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 proc_macro2::Span; + +use super::ExportItem; + +pub(crate) mod convert; +mod function; +mod impl_; + +use self::{function::gen_fn_metadata, impl_::gen_impl_metadata}; + +pub fn gen_metadata(item: syn::Item, mod_path: &[String]) -> syn::Result<ExportItem> { + match item { + syn::Item::Fn(item) => gen_fn_metadata(item.sig, mod_path), + syn::Item::Impl(item) => gen_impl_metadata(item, mod_path), + // FIXME: Support const / static? + _ => Err(syn::Error::new( + Span::call_site(), + "unsupported item: only functions and impl \ + blocks may be annotated with this attribute", + )), + } +} diff --git a/third_party/rust/uniffi_macros/src/export/metadata/convert.rs b/third_party/rust/uniffi_macros/src/export/metadata/convert.rs new file mode 100644 index 0000000000..c19ae579c2 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/convert.rs @@ -0,0 +1,222 @@ +/* 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 proc_macro2::Ident; +use quote::ToTokens; +use uniffi_meta::{FnParamMetadata, Type}; + +pub(super) fn fn_param_metadata(params: &[syn::FnArg]) -> syn::Result<Vec<FnParamMetadata>> { + params + .iter() + .filter_map(|a| { + let _is_method = false; + let (name, ty) = match a { + // methods currently have an implicit self parameter in uniffi_meta + syn::FnArg::Receiver(_) => return None, + syn::FnArg::Typed(pat_ty) => { + let name = match &*pat_ty.pat { + syn::Pat::Ident(pat_id) => pat_id.ident.to_string(), + _ => unimplemented!(), + }; + + // methods currently have an implicit self parameter in uniffi_meta + if name == "self" { + return None; + } + + (name, &pat_ty.ty) + } + }; + + Some(convert_type(ty).map(|ty| FnParamMetadata { name, ty })) + }) + .collect() +} + +pub(crate) fn convert_return_type(ty: &syn::Type) -> syn::Result<Option<Type>> { + match ty { + syn::Type::Tuple(tup) if tup.elems.is_empty() => Ok(None), + _ => convert_type(ty).map(Some), + } +} + +pub(crate) fn convert_type(ty: &syn::Type) -> syn::Result<Type> { + let type_path = type_as_type_path(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", + )); + } + + if type_path.path.segments.len() > 1 { + return Err(syn::Error::new_spanned( + type_path, + "qualified paths in types are not currently supported by uniffi::export", + )); + } + + match &type_path.path.segments.first() { + Some(seg) => match &seg.arguments { + syn::PathArguments::None => Ok(convert_bare_type_name(&seg.ident)), + syn::PathArguments::AngleBracketed(a) => convert_generic_type(&seg.ident, a), + syn::PathArguments::Parenthesized(_) => Err(type_not_supported(type_path)), + }, + None => Err(syn::Error::new_spanned( + type_path, + "unreachable: TypePath must have non-empty segments", + )), + } +} + +fn convert_generic_type( + ident: &Ident, + a: &syn::AngleBracketedGenericArguments, +) -> syn::Result<Type> { + let mut it = a.args.iter(); + match it.next() { + // `u8<>` is a valid way to write `u8` in the type namespace, so why not? + None => Ok(convert_bare_type_name(ident)), + Some(arg1) => match it.next() { + None => convert_generic_type1(ident, arg1), + Some(arg2) => match it.next() { + None => convert_generic_type2(ident, arg1, arg2), + Some(_) => Err(syn::Error::new_spanned( + ident, + "types with more than two generics are not currently + supported by uniffi::export", + )), + }, + }, + } +} + +fn convert_bare_type_name(ident: &Ident) -> Type { + let name = ident.to_string(); + match name.as_str() { + "u8" => Type::U8, + "u16" => Type::U16, + "u32" => Type::U32, + "u64" => Type::U64, + "i8" => Type::I8, + "i16" => Type::I16, + "i32" => Type::I32, + "i64" => Type::I64, + "f32" => Type::F32, + "f64" => Type::F64, + "bool" => Type::Bool, + "String" => Type::String, + _ => Type::Unresolved { name }, + } +} + +fn convert_generic_type1(ident: &Ident, arg: &syn::GenericArgument) -> syn::Result<Type> { + let arg = arg_as_type(arg)?; + match ident.to_string().as_str() { + "Arc" => Ok(Type::ArcObject { + object_name: type_as_type_name(arg)?.to_string(), + }), + "Option" => Ok(Type::Option { + inner_type: convert_type(arg)?.into(), + }), + "Vec" => Ok(Type::Vec { + inner_type: convert_type(arg)?.into(), + }), + _ => Err(type_not_supported(ident)), + } +} + +fn convert_generic_type2( + ident: &Ident, + arg1: &syn::GenericArgument, + arg2: &syn::GenericArgument, +) -> syn::Result<Type> { + let arg1 = arg_as_type(arg1)?; + let arg2 = arg_as_type(arg2)?; + + match ident.to_string().as_str() { + "HashMap" => Ok(Type::HashMap { + key_type: convert_type(arg1)?.into(), + value_type: convert_type(arg2)?.into(), + }), + _ => Err(type_not_supported(ident)), + } +} + +fn type_as_type_name(arg: &syn::Type) -> syn::Result<&Ident> { + type_as_type_path(arg)? + .path + .get_ident() + .ok_or_else(|| type_not_supported(arg)) +} + +pub(super) 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 arg_as_type(arg: &syn::GenericArgument) -> syn::Result<&syn::Type> { + match arg { + syn::GenericArgument::Type(t) => Ok(t), + _ => Err(syn::Error::new_spanned( + arg, + "non-type generic parameters are not currently supported by uniffi::export", + )), + } +} + +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", + ) +} + +pub(crate) fn try_split_result(ty: &syn::Type) -> syn::Result<Option<(&syn::Type, Ident)>> { + let type_path = type_as_type_path(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", + )); + } + + if type_path.path.segments.len() > 1 { + return Err(syn::Error::new_spanned( + type_path, + "qualified paths in types are not currently supported by uniffi::export", + )); + } + + let (ident, a) = match &type_path.path.segments.first() { + Some(seg) => match &seg.arguments { + syn::PathArguments::AngleBracketed(a) => (&seg.ident, a), + syn::PathArguments::None | syn::PathArguments::Parenthesized(_) => return Ok(None), + }, + None => return Ok(None), + }; + + let mut it = a.args.iter(); + if let Some(arg1) = it.next() { + if let Some(arg2) = it.next() { + if it.next().is_none() { + let arg1 = arg_as_type(arg1)?; + let arg2 = arg_as_type(arg2)?; + + if let "Result" = ident.to_string().as_str() { + let throws = type_as_type_name(arg2)?.to_owned(); + return Ok(Some((arg1, throws))); + } + } + } + } + + Ok(None) +} diff --git a/third_party/rust/uniffi_macros/src/export/metadata/function.rs b/third_party/rust/uniffi_macros/src/export/metadata/function.rs new file mode 100644 index 0000000000..a2cf6b7aa1 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/function.rs @@ -0,0 +1,32 @@ +/* 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 uniffi_meta::FnMetadata; + +use super::convert::{convert_return_type, fn_param_metadata}; +use crate::export::{ExportItem, Signature}; + +pub(super) fn gen_fn_metadata(sig: syn::Signature, mod_path: &[String]) -> syn::Result<ExportItem> { + let sig = Signature::new(sig)?; + let metadata = fn_metadata(&sig, mod_path)?; + Ok(ExportItem::Function { sig, metadata }) +} + +fn fn_metadata(sig: &Signature, mod_path: &[String]) -> syn::Result<FnMetadata> { + let (return_type, throws) = match &sig.output { + Some(ret) => ( + convert_return_type(&ret.ty)?, + ret.throws.as_ref().map(ToString::to_string), + ), + None => (None, None), + }; + + Ok(FnMetadata { + module_path: mod_path.to_owned(), + name: sig.ident.to_string(), + inputs: fn_param_metadata(&sig.inputs)?, + return_type, + throws, + }) +} diff --git a/third_party/rust/uniffi_macros/src/export/metadata/impl_.rs b/third_party/rust/uniffi_macros/src/export/metadata/impl_.rs new file mode 100644 index 0000000000..302f0bfa13 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/impl_.rs @@ -0,0 +1,93 @@ +/* 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 uniffi_meta::MethodMetadata; + +use super::convert::{convert_return_type, fn_param_metadata, type_as_type_path}; +use crate::export::{ExportItem, Method, Signature}; + +pub(super) fn gen_impl_metadata( + item: syn::ItemImpl, + mod_path: &[String], +) -> syn::Result<ExportItem> { + 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 methods = item + .items + .into_iter() + .map(|it| gen_method_metadata(it, &self_ident.to_string(), mod_path)) + .collect(); + + Ok(ExportItem::Impl { + methods, + self_ident: self_ident.to_owned(), + }) +} + +fn gen_method_metadata( + it: syn::ImplItem, + self_name: &str, + mod_path: &[String], +) -> syn::Result<Method> { + let sig = match it { + syn::ImplItem::Method(m) => Signature::new(m.sig)?, + _ => { + return Err(syn::Error::new_spanned( + it, + "only methods are supported in impl blocks annotated with uniffi::export", + )); + } + }; + + let metadata = method_metadata(self_name, &sig, mod_path)?; + + Ok(Method { sig, metadata }) +} + +fn method_metadata( + self_name: &str, + sig: &Signature, + mod_path: &[String], +) -> syn::Result<MethodMetadata> { + let (return_type, throws) = match &sig.output { + Some(ret) => ( + convert_return_type(&ret.ty)?, + ret.throws.as_ref().map(ToString::to_string), + ), + None => (None, None), + }; + + Ok(MethodMetadata { + module_path: mod_path.to_owned(), + self_name: self_name.to_owned(), + name: sig.ident.to_string(), + inputs: fn_param_metadata(&sig.inputs)?, + return_type, + throws, + }) +} diff --git a/third_party/rust/uniffi_macros/src/export/scaffolding.rs b/third_party/rust/uniffi_macros/src/export/scaffolding.rs new file mode 100644 index 0000000000..f66e8bf844 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/scaffolding.rs @@ -0,0 +1,199 @@ +/* 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 proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use syn::{parse_quote, FnArg, Pat}; + +use super::{FunctionReturn, Signature}; + +pub(super) fn gen_fn_scaffolding( + sig: &Signature, + mod_path: &[String], + checksum: u16, +) -> TokenStream { + let name = &sig.ident; + let name_s = name.to_string(); + + let ffi_ident = Ident::new( + &uniffi_meta::fn_ffi_symbol_name(mod_path, &name_s, checksum), + Span::call_site(), + ); + + const ERROR_MSG: &str = + "uniffi::export must be used on the impl block, not its containing fn's"; + let (params, args): (Vec<_>, Vec<_>) = collect_params(&sig.inputs, ERROR_MSG).unzip(); + + let fn_call = quote! { + #name(#(#args),*) + }; + + gen_ffi_function(sig, ffi_ident, ¶ms, fn_call) +} + +pub(super) fn gen_method_scaffolding( + sig: &Signature, + mod_path: &[String], + checksum: u16, + self_ident: &Ident, +) -> TokenStream { + let name = &sig.ident; + let name_s = name.to_string(); + + let ffi_name = format!("impl_{self_ident}_{name_s}"); + let ffi_ident = Ident::new( + &uniffi_meta::fn_ffi_symbol_name(mod_path, &ffi_name, checksum), + Span::call_site(), + ); + + let mut params_args = (Vec::new(), Vec::new()); + + const RECEIVER_ERROR: &str = "unreachable: only first parameter can be method receiver"; + let mut assoc_fn_error = None; + let fn_call_prefix = match sig.inputs.first() { + Some(arg) if is_receiver(arg) => { + let ffi_converter = quote! { + <::std::sync::Arc<#self_ident> as ::uniffi::FfiConverter> + }; + + params_args.0.push(quote! { this: #ffi_converter::FfiType }); + + let remaining_args = sig.inputs.iter().skip(1); + params_args.extend(collect_params(remaining_args, RECEIVER_ERROR)); + + quote! { + #ffi_converter::try_lift(this).unwrap_or_else(|err| { + ::std::panic!("Failed to convert arg 'self': {}", err) + }). + } + } + _ => { + assoc_fn_error = Some( + syn::Error::new_spanned( + &sig.ident, + "associated functions are not currently supported", + ) + .into_compile_error(), + ); + params_args.extend(collect_params(&sig.inputs, RECEIVER_ERROR)); + quote! { #self_ident:: } + } + }; + + let (params, args) = params_args; + + let fn_call = quote! { + #assoc_fn_error + #fn_call_prefix #name(#(#args),*) + }; + + gen_ffi_function(sig, ffi_ident, ¶ms, fn_call) +} + +fn is_receiver(fn_arg: &FnArg) -> bool { + match fn_arg { + FnArg::Receiver(_) => true, + FnArg::Typed(pat_ty) => matches!(&*pat_ty.pat, Pat::Ident(i) if i.ident == "self"), + } +} + +fn collect_params<'a>( + inputs: impl IntoIterator<Item = &'a FnArg> + 'a, + receiver_error_msg: &'static str, +) -> impl Iterator<Item = (TokenStream, TokenStream)> + 'a { + fn receiver_error( + receiver: impl ToTokens, + receiver_error_msg: &str, + ) -> (TokenStream, TokenStream) { + let param = quote! { &self }; + let arg = syn::Error::new_spanned(receiver, receiver_error_msg).into_compile_error(); + (param, arg) + } + + inputs.into_iter().enumerate().map(|(i, arg)| { + let (ty, name) = match arg { + FnArg::Receiver(r) => { + return receiver_error(r, receiver_error_msg); + } + FnArg::Typed(pat_ty) => { + let name = match &*pat_ty.pat { + Pat::Ident(i) if i.ident == "self" => { + return receiver_error(i, receiver_error_msg); + } + Pat::Ident(i) => Some(i.ident.to_string()), + _ => None, + }; + + (&pat_ty.ty, name) + } + }; + + let arg_n = format_ident!("arg{i}"); + let param = quote! { #arg_n: <#ty as ::uniffi::FfiConverter>::FfiType }; + + // FIXME: With UDL, fallible functions use uniffi::lower_anyhow_error_or_panic instead of + // panicking unconditionally. This seems cleaner though. + let panic_fmt = match name { + Some(name) => format!("Failed to convert arg '{name}': {{}}"), + None => format!("Failed to convert arg #{i}: {{}}"), + }; + let arg = quote! { + <#ty as ::uniffi::FfiConverter>::try_lift(#arg_n).unwrap_or_else(|err| { + ::std::panic!(#panic_fmt, err) + }) + }; + + (param, arg) + }) +} + +fn gen_ffi_function( + sig: &Signature, + ffi_ident: Ident, + params: &[TokenStream], + rust_fn_call: TokenStream, +) -> TokenStream { + let name = &sig.ident; + let name_s = name.to_string(); + + let unit_slot; + let (ty, throws) = match &sig.output { + Some(FunctionReturn { ty, throws }) => (ty, throws), + None => { + unit_slot = parse_quote! { () }; + (&unit_slot, &None) + } + }; + + let return_expr = if let Some(error_ident) = throws { + quote! { + ::uniffi::call_with_result(call_status, || { + let val = #rust_fn_call.map_err(|e| { + <#error_ident as ::uniffi::FfiConverter>::lower( + ::std::convert::Into::into(e), + ) + })?; + Ok(<#ty as ::uniffi::FfiReturn>::lower(val)) + }) + } + } else { + quote! { + ::uniffi::call_with_output(call_status, || { + <#ty as ::uniffi::FfiReturn>::lower(#rust_fn_call) + }) + } + }; + + quote! { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #ffi_ident( + #(#params,)* + call_status: &mut ::uniffi::RustCallStatus, + ) -> <#ty as ::uniffi::FfiReturn>::FfiType { + ::uniffi::deps::log::debug!(#name_s); + #return_expr + } + } +} diff --git a/third_party/rust/uniffi_macros/src/lib.rs b/third_party/rust/uniffi_macros/src/lib.rs new file mode 100644 index 0000000000..a38705e57d --- /dev/null +++ b/third_party/rust/uniffi_macros/src/lib.rs @@ -0,0 +1,179 @@ +/* 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/. */ +#![cfg_attr(feature = "nightly", feature(proc_macro_expand))] + +//! Macros for `uniffi`. +//! +//! Currently this is just for easily generating integration tests, but maybe +//! we'll put some other code-annotation helper macros in here at some point. + +use camino::Utf8Path; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, LitStr}; +use util::rewrite_self_type; + +mod enum_; +mod error; +mod export; +mod object; +mod record; +mod test; +mod util; + +use self::{ + enum_::expand_enum, error::expand_error, export::expand_export, object::expand_object, + record::expand_record, +}; + +/// A macro to build testcases for a component's generated bindings. +/// +/// This macro provides some plumbing to write automated tests for the generated +/// foreign language bindings of a component. As a component author, you can write +/// script files in the target foreign language(s) that exercise you component API, +/// and then call this macro to produce a `cargo test` testcase from each one. +/// The generated code will execute your script file with appropriate configuration and +/// environment to let it load the component bindings, and will pass iff the script +/// exits successfully. +/// +/// To use it, invoke the macro with the name of a fixture/example crate as the first argument, +/// then one or more file paths relative to the crate root directory. It will produce one `#[test]` +/// function per file, in a manner designed to play nicely with `cargo test` and its test filtering +/// options. +#[proc_macro] +pub fn build_foreign_language_testcases(tokens: TokenStream) -> TokenStream { + test::build_foreign_language_testcases(tokens) +} + +#[proc_macro_attribute] +pub fn export(_attr: TokenStream, input: TokenStream) -> TokenStream { + let input2 = proc_macro2::TokenStream::from(input.clone()); + + let gen_output = || { + let mod_path = util::mod_path()?; + let mut item = syn::parse(input)?; + + // If the input is an `impl` block, rewrite any uses of the `Self` type + // alias to the actual type, so we don't have to special-case it in the + // metadata collection or scaffolding code generation (which generates + // new functions outside of the `impl`). + rewrite_self_type(&mut item); + + let metadata = export::gen_metadata(item, &mod_path)?; + Ok(expand_export(metadata, &mod_path)) + }; + let output = gen_output().unwrap_or_else(syn::Error::into_compile_error); + + quote! { + #input2 + #output + } + .into() +} + +#[proc_macro_derive(Record)] +pub fn derive_record(input: TokenStream) -> TokenStream { + let mod_path = match util::mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error().into(), + }; + let input = parse_macro_input!(input); + + expand_record(input, mod_path).into() +} + +#[proc_macro_derive(Enum)] +pub fn derive_enum(input: TokenStream) -> TokenStream { + let mod_path = match util::mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error().into(), + }; + let input = parse_macro_input!(input); + + expand_enum(input, mod_path).into() +} + +#[proc_macro_derive(Object)] +pub fn derive_object(input: TokenStream) -> TokenStream { + let mod_path = match util::mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error().into(), + }; + let input = parse_macro_input!(input); + + expand_object(input, mod_path).into() +} + +#[proc_macro_derive(Error, attributes(uniffi))] +pub fn derive_error(input: TokenStream) -> TokenStream { + let mod_path = match util::mod_path() { + Ok(p) => p, + Err(e) => return e.into_compile_error().into(), + }; + let input = parse_macro_input!(input); + + expand_error(input, mod_path).into() +} + +/// A helper macro to include generated component scaffolding. +/// +/// This is a simple convenience macro to include the UniFFI component +/// scaffolding as built by `uniffi_build::generate_scaffolding`. +/// Use it like so: +/// +/// ```rs +/// uniffi_macros::include_scaffolding!("my_component_name"); +/// ``` +/// +/// This will expand to the appropriate `include!` invocation to include +/// the generated `my_component_name.uniffi.rs` (which it assumes has +/// been successfully built by your crate's `build.rs` script). +/// +#[proc_macro] +pub fn include_scaffolding(component_name: TokenStream) -> TokenStream { + let name = syn::parse_macro_input!(component_name as syn::LitStr); + if std::env::var("OUT_DIR").is_err() { + quote! { + compile_error!("This macro assumes the crate has a build.rs script, but $OUT_DIR is not present"); + } + } else { + quote! { + include!(concat!(env!("OUT_DIR"), "/", #name, ".uniffi.rs")); + } + }.into() +} + +/// A helper macro to generate and include component scaffolding. +/// +/// This is a convenience macro designed for writing `trybuild`-style tests and +/// probably shouldn't be used for production code. Given the path to a `.udl` file, +/// if will run `uniffi-bindgen` to produce the corresponding Rust scaffolding and then +/// include it directly into the calling file. Like so: +/// +/// ```rs +/// uniffi_macros::generate_and_include_scaffolding!("path/to/my/interface.udl"); +/// ``` +/// +#[proc_macro] +pub fn generate_and_include_scaffolding(udl_file: TokenStream) -> TokenStream { + let udl_file = syn::parse_macro_input!(udl_file as syn::LitStr); + let udl_file_string = udl_file.value(); + let udl_file_path = Utf8Path::new(&udl_file_string); + if std::env::var("OUT_DIR").is_err() { + quote! { + compile_error!("This macro assumes the crate has a build.rs script, but $OUT_DIR is not present"); + } + } else if uniffi_build::generate_scaffolding(udl_file_path).is_err() { + quote! { + compile_error!(concat!("Failed to generate scaffolding from UDL file at ", #udl_file)); + } + } else { + // We know the filename is good because `generate_scaffolding` succeeded, + // so this `unwrap` will never fail. + let name = LitStr::new(udl_file_path.file_stem().unwrap(), udl_file.span()); + quote! { + uniffi_macros::include_scaffolding!(#name); + } + }.into() +} diff --git a/third_party/rust/uniffi_macros/src/object.rs b/third_party/rust/uniffi_macros/src/object.rs new file mode 100644 index 0000000000..2f4988530c --- /dev/null +++ b/third_party/rust/uniffi_macros/src/object.rs @@ -0,0 +1,35 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::DeriveInput; +use uniffi_meta::ObjectMetadata; + +use crate::util::{assert_type_eq, create_metadata_static_var}; + +pub fn expand_object(input: DeriveInput, module_path: Vec<String>) -> TokenStream { + let ident = &input.ident; + let name = ident.to_string(); + let metadata = ObjectMetadata { module_path, name }; + let free_fn_ident = Ident::new(&metadata.free_ffi_symbol_name(), Span::call_site()); + let meta_static_var = create_metadata_static_var(ident, metadata.into()); + let type_assertion = assert_type_eq(ident, quote! { crate::uniffi_types::#ident }); + + quote! { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #free_fn_ident( + ptr: *const ::std::ffi::c_void, + call_status: &mut ::uniffi::RustCallStatus + ) { + uniffi::call_with_output(call_status, || { + assert!(!ptr.is_null()); + let ptr = ptr.cast::<#ident>(); + unsafe { + ::std::sync::Arc::decrement_strong_count(ptr); + } + }); + } + + #meta_static_var + #type_assertion + } +} diff --git a/third_party/rust/uniffi_macros/src/record.rs b/third_party/rust/uniffi_macros/src/record.rs new file mode 100644 index 0000000000..60d0962503 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/record.rs @@ -0,0 +1,108 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{Data, DeriveInput, Field, Fields}; +use uniffi_meta::{FieldMetadata, RecordMetadata}; + +use crate::{ + export::metadata::convert::convert_type, + util::{assert_type_eq, create_metadata_static_var, try_read_field}, +}; + +pub fn expand_record(input: DeriveInput, module_path: Vec<String>) -> TokenStream { + let fields = match input.data { + Data::Struct(s) => Some(s.fields), + _ => None, + }; + + let ident = &input.ident; + + let (write_impl, try_read_fields) = match &fields { + Some(fields) => ( + fields.iter().map(write_field).collect(), + fields.iter().map(try_read_field).collect(), + ), + None => { + let unimplemented = quote! { ::std::unimplemented!() }; + (unimplemented.clone(), unimplemented) + } + }; + + let meta_static_var = if let Some(fields) = fields { + match record_metadata(ident, fields, module_path) { + Ok(metadata) => create_metadata_static_var(ident, metadata.into()), + Err(e) => e.into_compile_error(), + } + } else { + syn::Error::new( + Span::call_site(), + "This derive must only be used on structs", + ) + .into_compile_error() + }; + + let type_assertion = assert_type_eq(ident, quote! { crate::uniffi_types::#ident }); + + quote! { + #[automatically_derived] + impl ::uniffi::RustBufferFfiConverter for #ident { + type RustType = Self; + + fn write(obj: Self, buf: &mut ::std::vec::Vec<u8>) { + #write_impl + } + + fn try_read(buf: &mut &[::std::primitive::u8]) -> ::uniffi::deps::anyhow::Result<Self> { + Ok(Self { #try_read_fields }) + } + } + + #meta_static_var + #type_assertion + } +} + +fn record_metadata( + ident: &Ident, + fields: Fields, + module_path: Vec<String>, +) -> syn::Result<RecordMetadata> { + let name = ident.to_string(); + let fields = match fields { + Fields::Named(fields) => fields.named, + _ => { + return Err(syn::Error::new( + Span::call_site(), + "UniFFI only supports structs with named fields", + )); + } + }; + + let fields = fields + .iter() + .map(field_metadata) + .collect::<syn::Result<_>>()?; + + Ok(RecordMetadata { + module_path, + name, + fields, + }) +} + +fn field_metadata(f: &Field) -> syn::Result<FieldMetadata> { + let name = f.ident.as_ref().unwrap().to_string(); + + Ok(FieldMetadata { + name, + ty: convert_type(&f.ty)?, + }) +} + +fn write_field(f: &Field) -> TokenStream { + let ident = &f.ident; + let ty = &f.ty; + + quote! { + <#ty as ::uniffi::FfiConverter>::write(obj.#ident, buf); + } +} diff --git a/third_party/rust/uniffi_macros/src/test.rs b/third_party/rust/uniffi_macros/src/test.rs new file mode 100644 index 0000000000..ddcbf46000 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/test.rs @@ -0,0 +1,90 @@ +/* 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 camino::{Utf8Path, Utf8PathBuf}; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use std::env; +use syn::{parse_macro_input, punctuated::Punctuated, LitStr, Token}; + +pub(crate) fn build_foreign_language_testcases(tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as BuildForeignLanguageTestCaseInput); + // we resolve each path relative to the crate root directory. + let pkg_dir = env::var("CARGO_MANIFEST_DIR") + .expect("Missing $CARGO_MANIFEST_DIR, cannot build tests for generated bindings"); + + // For each test file found, generate a matching testcase. + let test_functions = input + .test_scripts + .iter() + .map(|file_path| { + let test_file_pathbuf: Utf8PathBuf = [&pkg_dir, file_path].iter().collect(); + let test_file_path = test_file_pathbuf.to_string(); + let test_file_name = test_file_pathbuf + .file_name() + .expect("Test file has no name, cannot build tests for generated bindings"); + let test_name = format_ident!( + "uniffi_foreign_language_testcase_{}", + test_file_name.replace(|c: char| !c.is_alphanumeric(), "_") + ); + let run_test = match test_file_pathbuf.extension() { + Some("kts") => quote! { + uniffi::kotlin_run_test + }, + Some("swift") => quote! { + uniffi::swift_run_test + }, + Some("py") => quote! { + uniffi::python_run_test + }, + Some("rb") => quote! { + uniffi::ruby_run_test + }, + _ => panic!("Unexpected extension for test script: {test_file_name}"), + }; + let maybe_ignore = if should_skip_path(&test_file_pathbuf) { + quote! { #[ignore] } + } else { + quote! {} + }; + quote! { + #maybe_ignore + #[test] + fn #test_name () -> uniffi::deps::anyhow::Result<()> { + #run_test( + std::env!("CARGO_TARGET_TMPDIR"), + std::env!("CARGO_PKG_NAME"), + #test_file_path) + } + } + }) + .collect::<Vec<proc_macro2::TokenStream>>(); + let test_module = quote! { + #(#test_functions)* + }; + TokenStream::from(test_module) +} + +// UNIFFI_TESTS_DISABLE_EXTENSIONS contains a comma-sep'd list of extensions (without leading `.`) +fn should_skip_path(path: &Utf8Path) -> bool { + let ext = path.extension().expect("File has no extension!"); + env::var("UNIFFI_TESTS_DISABLE_EXTENSIONS") + .map(|v| v.split(',').any(|look| look == ext)) + .unwrap_or(false) +} + +struct BuildForeignLanguageTestCaseInput { + test_scripts: Vec<String>, +} + +impl syn::parse::Parse for BuildForeignLanguageTestCaseInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let test_scripts = Punctuated::<LitStr, Token![,]>::parse_terminated(input)? + .iter() + .map(|s| s.value()) + .collect(); + + Ok(Self { test_scripts }) + } +} diff --git a/third_party/rust/uniffi_macros/src/util.rs b/third_party/rust/uniffi_macros/src/util.rs new file mode 100644 index 0000000000..e8e03b3b5d --- /dev/null +++ b/third_party/rust/uniffi_macros/src/util.rs @@ -0,0 +1,223 @@ +/* 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 proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + visit_mut::VisitMut, + Attribute, Item, Token, Type, +}; +use uniffi_meta::Metadata; + +#[cfg(not(feature = "nightly"))] +pub fn mod_path() -> syn::Result<Vec<String>> { + // Without the nightly feature and TokenStream::expand_expr, just return the crate name + + use std::path::Path; + + use fs_err as fs; + use once_cell::sync::Lazy; + use serde::Deserialize; + + #[derive(Deserialize)] + struct CargoToml { + package: Package, + #[serde(default)] + lib: Lib, + } + + #[derive(Deserialize)] + struct Package { + name: String, + } + + #[derive(Default, Deserialize)] + struct Lib { + name: Option<String>, + } + + static LIB_CRATE_MOD_PATH: Lazy<Result<Vec<String>, String>> = Lazy::new(|| { + let manifest_dir = + std::env::var_os("CARGO_MANIFEST_DIR").ok_or("`CARGO_MANIFEST_DIR` is not set")?; + + let cargo_toml_bytes = + fs::read(Path::new(&manifest_dir).join("Cargo.toml")).map_err(|e| e.to_string())?; + let cargo_toml = toml::from_slice::<CargoToml>(&cargo_toml_bytes) + .map_err(|e| format!("Failed to parse `Cargo.toml`: {e}"))?; + + let lib_crate_name = cargo_toml + .lib + .name + .unwrap_or_else(|| cargo_toml.package.name.replace('-', "_")); + + Ok(vec![lib_crate_name]) + }); + + LIB_CRATE_MOD_PATH + .clone() + .map_err(|e| syn::Error::new(Span::call_site(), e)) +} + +#[cfg(feature = "nightly")] +pub fn mod_path() -> syn::Result<Vec<String>> { + use proc_macro::TokenStream; + + let module_path_invoc = TokenStream::from(quote! { ::core::module_path!() }); + // We ask the compiler what `module_path!()` expands to here. + // This is a nightly feature, tracked at https://github.com/rust-lang/rust/issues/90765 + let expanded_module_path = TokenStream::expand_expr(&module_path_invoc) + .map_err(|e| syn::Error::new(Span::call_site(), e))?; + Ok(syn::parse::<syn::LitStr>(expanded_module_path)? + .value() + .split("::") + .collect()) +} + +/// Rewrite Self type alias usage in an impl block to the type itself. +/// +/// For example, +/// +/// ```ignore +/// impl some::module::Foo { +/// fn method( +/// self: Arc<Self>, +/// arg: Option<Bar<(), Self>>, +/// ) -> Result<Self, Error> { +/// todo!() +/// } +/// } +/// ``` +/// +/// will be rewritten to +/// +/// ```ignore +/// impl some::module::Foo { +/// fn method( +/// self: Arc<some::module::Foo>, +/// arg: Option<Bar<(), some::module::Foo>>, +/// ) -> Result<some::module::Foo, Error> { +/// todo!() +/// } +/// } +/// ``` +pub fn rewrite_self_type(item: &mut Item) { + let item = match item { + Item::Impl(i) => i, + _ => return, + }; + + struct RewriteSelfVisitor<'a>(&'a Type); + + impl<'a> VisitMut for RewriteSelfVisitor<'a> { + fn visit_type_mut(&mut self, i: &mut Type) { + match i { + Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self") => { + *i = self.0.clone(); + } + _ => syn::visit_mut::visit_type_mut(self, i), + } + } + } + + let mut visitor = RewriteSelfVisitor(&item.self_ty); + for item in &mut item.items { + visitor.visit_impl_item_mut(item); + } +} + +pub fn try_read_field(f: &syn::Field) -> TokenStream { + let ident = &f.ident; + let ty = &f.ty; + + quote! { + #ident: <#ty as ::uniffi::FfiConverter>::try_read(buf)?, + } +} + +pub fn create_metadata_static_var(name: &Ident, val: Metadata) -> TokenStream { + let data: Vec<u8> = bincode::serialize(&val).expect("Error serializing metadata item"); + let count = data.len(); + let var_name = format_ident!("UNIFFI_META_{}", name); + + quote! { + #[no_mangle] + #[doc(hidden)] + pub static #var_name: [u8; #count] = [#(#data),*]; + } +} + +pub fn assert_type_eq(a: impl ToTokens + Spanned, b: impl ToTokens) -> TokenStream { + quote_spanned! {a.span()=> + #[allow(unused_qualifications)] + const _: () = { + ::uniffi::deps::static_assertions::assert_type_eq_all!(#a, #b); + }; + } +} + +pub fn chain<T>( + a: impl IntoIterator<Item = T>, + b: impl IntoIterator<Item = T>, +) -> impl Iterator<Item = T> { + a.into_iter().chain(b) +} + +pub trait UniffiAttribute: Default + Parse { + fn merge(self, other: Self) -> syn::Result<Self>; +} + +#[derive(Default)] +struct AttributeNotAllowedHere; + +impl Parse for AttributeNotAllowedHere { + fn parse(input: ParseStream<'_>) -> syn::Result<Self> { + Err(syn::Error::new( + input.span(), + "UniFFI attributes are not currently recognized in this position", + )) + } +} + +impl UniffiAttribute for AttributeNotAllowedHere { + fn merge(self, _other: Self) -> syn::Result<Self> { + Ok(Self) + } +} + +pub trait AttributeSliceExt { + fn parse_uniffi_attributes<T: UniffiAttribute>(&self) -> syn::Result<T>; + fn attributes_not_allowed_here(&self) -> Option<syn::Error>; +} + +impl AttributeSliceExt for [Attribute] { + fn parse_uniffi_attributes<T: UniffiAttribute>(&self) -> syn::Result<T> { + self.iter() + .filter(|attr| attr.path.is_ident("uniffi")) + .try_fold(T::default(), |res, attr| { + let list: Punctuated<T, Token![,]> = + attr.parse_args_with(Punctuated::parse_terminated)?; + list.into_iter().try_fold(res, T::merge) + }) + } + + fn attributes_not_allowed_here(&self) -> Option<syn::Error> { + self.parse_uniffi_attributes::<AttributeNotAllowedHere>() + .err() + } +} + +pub fn either_attribute_arg<T: ToTokens>(a: Option<T>, b: Option<T>) -> syn::Result<Option<T>> { + match (a, b) { + (None, None) => Ok(None), + (Some(val), None) | (None, Some(val)) => Ok(Some(val)), + (Some(a), Some(b)) => { + let mut error = syn::Error::new_spanned(a, "redundant attribute argument"); + error.combine(syn::Error::new_spanned(b, "note: first one here")); + Err(error) + } + } +} |