diff options
Diffstat (limited to 'third_party/rust/uniffi_macros')
-rw-r--r-- | third_party/rust/uniffi_macros/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/Cargo.toml | 70 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export.rs | 163 | ||||
-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 | 179 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export/metadata/function.rs | 26 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export/metadata/impl_.rs | 84 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/export/scaffolding.rs | 184 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/lib.rs | 238 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/object.rs | 35 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/record.rs | 100 | ||||
-rw-r--r-- | third_party/rust/uniffi_macros/src/util.rs | 146 |
12 files changed, 1252 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..0e3ebe4a5a --- /dev/null +++ b/third_party/rust/uniffi_macros/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"6511b493b676ac3941d70477c91abec62642c3c9aab088ecdf4f733eef3f1faa","src/export.rs":"e23929cf6fb5542d29514fe668f3b3d836fad968eacd9c6fcba74c5cd9cf2b61","src/export/metadata.rs":"af89a9942c7c0c4043a3cd57d1e6bd71cde19005e1f9f246efac761f47eff6be","src/export/metadata/convert.rs":"81060fb3390165d77db021f44142a2f3f10882515f859d7393857083370f2d35","src/export/metadata/function.rs":"11833cabd37e7671c0a01944bec73b8892a15df814bbe4c26fdae57aad89a2ba","src/export/metadata/impl_.rs":"ecfdaa132f05dd946414281e52165ef19c90c0bfd76ec651d4ec86837bd41d1c","src/export/scaffolding.rs":"66939405063e56fc983126f249e2d7ddc3257cb045a738abd0cf813a4aafc59c","src/lib.rs":"ca77b437a58cfb3ddeb106d3c1c8378545c46ef241298e62ab1190c5136d1fb1","src/object.rs":"955b596f344304013692042bdc1760bbb1192ec33950b0dd2932cb8de94ec297","src/record.rs":"67a5c7ed6a448f7ad8f5c8e930c5e3007b2b0cac32f52cc8596bdae6fb3c816e","src/util.rs":"6389a9b4258808a3af168cf85658fb7c069172d5e528ee0e94210fa664f2a414"},"package":"c96a574677566f83ea8458dac1dd7792fd63e7c3f9dbcd865f0e8d6f8057b127"}
\ 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..af38af08f4 --- /dev/null +++ b/third_party/rust/uniffi_macros/Cargo.toml @@ -0,0 +1,70 @@ +# 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.21.1" +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.21.1" + +[dependencies.uniffi_meta] +version = "=0.21.1" + +[features] +builtin-bindgen = ["uniffi_build/builtin-bindgen"] +default = [] +nightly = [] 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..0910f1f602 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export.rs @@ -0,0 +1,163 @@ +/* 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::scaffolding::{gen_fn_scaffolding, gen_method_scaffolding}; +use crate::{ + export::metadata::convert::convert_type, + util::{assert_type_eq, create_metadata_static_var}, +}; + +// TODO(jplatte): Ensure no generics, no async, … +// TODO(jplatte): Aggregate errors instead of short-circuiting, whereever possible + +pub enum ExportItem { + Function { + sig: Box<syn::Signature>, + metadata: FnMetadata, + }, + Impl { + self_ident: Ident, + methods: Vec<syn::Result<Method>>, + }, +} + +pub struct Method { + item: syn::ImplItemMethod, + metadata: MethodMetadata, +} + +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 { item, metadata }| { + let checksum = checksum(&metadata); + let scaffolding = + gen_method_scaffolding(&item.sig, mod_path, checksum, &self_ident); + let type_assertions = fn_type_assertions(&item.sig); + let meta_static_var = create_metadata_static_var( + &format_ident!("{}_{}", metadata.self_name, item.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: &syn::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 = match &sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => Some(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(); + + type_assertions.into_values().collect() +} 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..2d0027b695 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/convert.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/. */ + +use proc_macro2::Ident; +use quote::ToTokens; +use syn::{punctuated::Punctuated, Token}; +use uniffi_meta::{FnParamMetadata, Type}; + +pub(super) fn fn_param_metadata( + params: &Punctuated<syn::FnArg, Token![,]>, +) -> 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(super) fn return_type_metadata(ty: &syn::ReturnType) -> syn::Result<Option<Type>> { + Ok(match ty { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => Some(convert_type(ty)?), + }) +} + +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_path(arg)? + .path + .get_ident() + .ok_or_else(|| type_not_supported(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)), + } +} + +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", + ) +} 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..b19e8108c6 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/function.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 uniffi_meta::FnMetadata; + +use super::convert::{fn_param_metadata, return_type_metadata}; +use crate::export::ExportItem; + +pub(super) fn gen_fn_metadata(sig: syn::Signature, mod_path: &[String]) -> syn::Result<ExportItem> { + let metadata = fn_metadata(&sig, mod_path)?; + + Ok(ExportItem::Function { + sig: Box::new(sig), + metadata, + }) +} + +fn fn_metadata(sig: &syn::Signature, mod_path: &[String]) -> syn::Result<FnMetadata> { + Ok(FnMetadata { + module_path: mod_path.to_owned(), + name: sig.ident.to_string(), + inputs: fn_param_metadata(&sig.inputs)?, + return_type: return_type_metadata(&sig.output)?, + }) +} 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..33709ba692 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/metadata/impl_.rs @@ -0,0 +1,84 @@ +/* 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::{fn_param_metadata, return_type_metadata, type_as_type_path}; +use crate::export::{ExportItem, Method}; + +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 item = match it { + syn::ImplItem::Method(m) => m, + _ => { + return Err(syn::Error::new_spanned( + it, + "only methods are supported in impl blocks annotated with uniffi::export", + )); + } + }; + + let metadata = method_metadata(self_name, &item, mod_path)?; + + Ok(Method { item, metadata }) +} + +fn method_metadata( + self_name: &str, + f: &syn::ImplItemMethod, + mod_path: &[String], +) -> syn::Result<MethodMetadata> { + Ok(MethodMetadata { + module_path: mod_path.to_owned(), + self_name: self_name.to_owned(), + name: f.sig.ident.to_string(), + inputs: fn_param_metadata(&f.sig.inputs)?, + return_type: return_type_metadata(&f.sig.output)?, + }) +} 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..8d46a3aec5 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/export/scaffolding.rs @@ -0,0 +1,184 @@ +/* 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::{FnArg, Pat, ReturnType, 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: &syn::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, "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 }; + + 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: &syn::Signature, + ffi_ident: Ident, + params: &[TokenStream], + rust_fn_call: TokenStream, +) -> TokenStream { + let name = &sig.ident; + let name_s = name.to_string(); + + // FIXME(jplatte): Use an extra trait implemented for `T: FfiConverter` as + // well as `()` so no different codegen is needed? + let (output, return_expr); + match &sig.output { + ReturnType::Default => { + output = None; + return_expr = rust_fn_call; + } + ReturnType::Type(_, ty) => { + output = Some(quote! { + -> <#ty as ::uniffi::FfiConverter>::FfiType + }); + return_expr = quote! { + <#ty as ::uniffi::FfiConverter>::lower(#rust_fn_call) + }; + } + } + + quote! { + #[doc(hidden)] + #[no_mangle] + pub extern "C" fn #ffi_ident( + #(#params,)* + call_status: &mut ::uniffi::RustCallStatus, + ) #output { + ::uniffi::deps::log::debug!(#name_s); + ::uniffi::call_with_output(call_status, || { + #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..fb19d0711c --- /dev/null +++ b/third_party/rust/uniffi_macros/src/lib.rs @@ -0,0 +1,238 @@ +/* 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, Utf8PathBuf}; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use std::env; +use syn::{bracketed, parse_macro_input, punctuated::Punctuated, LitStr, Token}; +use util::rewrite_self_type; + +mod export; +mod object; +mod record; +mod util; + +use self::{export::expand_export, object::expand_object, record::expand_record}; + +#[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(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() +} + +/// 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 one or more udl files 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(paths: TokenStream) -> TokenStream { + let paths = syn::parse_macro_input!(paths as FilePaths); + // 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"); + + // Create an array of UDL files. + let udl_files = &paths + .udl_files + .iter() + .map(|file_path| { + let pathbuf: Utf8PathBuf = [&pkg_dir, file_path].iter().collect(); + let path = pathbuf.to_string(); + quote! { #path } + }) + .collect::<Vec<proc_macro2::TokenStream>>(); + + // For each test file found, generate a matching testcase. + let test_functions = paths.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 maybe_ignore = if should_skip_path(&test_file_pathbuf) { + quote! { #[ignore] } + } else { + quote! { } + }; + quote! { + #maybe_ignore + #[test] + fn #test_name () -> uniffi::deps::anyhow::Result<()> { + uniffi::testing::run_foreign_language_testcase(#pkg_dir, &[ #(#udl_files),* ], #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) +} + +/// Newtype to simplifying parsing a list of file paths from macro input. +#[derive(Debug)] +struct FilePaths { + udl_files: Vec<String>, + test_scripts: Vec<String>, +} + +impl syn::parse::Parse for FilePaths { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let udl_array; + bracketed!(udl_array in input); + let udl_files = Punctuated::<LitStr, Token![,]>::parse_terminated(&udl_array)? + .iter() + .map(|s| s.value()) + .collect(); + + let _comma: Token![,] = input.parse()?; + + let scripts_array; + bracketed!(scripts_array in input); + let test_scripts = Punctuated::<LitStr, Token![,]>::parse_terminated(&scripts_array)? + .iter() + .map(|s| s.value()) + .collect(); + + Ok(FilePaths { + udl_files, + test_scripts, + }) + } +} + +/// 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..b8bbc3e10e --- /dev/null +++ b/third_party/rust/uniffi_macros/src/record.rs @@ -0,0 +1,100 @@ +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{Data, DeriveInput}; +use uniffi_meta::{FieldMetadata, RecordMetadata}; + +use crate::{ + export::metadata::convert::convert_type, + util::{assert_type_eq, create_metadata_static_var}, +}; + +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(|f| { + let ident = &f.ident; + let ty = &f.ty; + + let write_field = quote! { + <#ty as ::uniffi::FfiConverter>::write(obj.#ident, buf); + }; + let try_read_field = quote! { + #ident: <#ty as ::uniffi::FfiConverter>::try_read(buf)?, + }; + + (write_field, try_read_field) + }) + .unzip(), + None => { + let unimplemented = quote! { ::std::unimplemented!() }; + (unimplemented.clone(), unimplemented) + } + }; + + let meta_static_var = fields + .map(|fields| { + let name = ident.to_string(); + let fields_res: syn::Result<_> = fields + .iter() + .map(|f| { + let name = f + .ident + .as_ref() + .expect("We only allow record structs") + .to_string(); + + Ok(FieldMetadata { + name, + ty: convert_type(&f.ty)?, + }) + }) + .collect(); + + match fields_res { + Ok(fields) => { + let metadata = RecordMetadata { + module_path, + name, + fields, + }; + + create_metadata_static_var(ident, metadata.into()) + } + Err(e) => e.into_compile_error(), + } + }) + .unwrap_or_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! { + 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 + } +} 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..32ca08fc79 --- /dev/null +++ b/third_party/rust/uniffi_macros/src/util.rs @@ -0,0 +1,146 @@ +/* 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::{spanned::Spanned, visit_mut::VisitMut, Item, 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; + use quote::quote; + + 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 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); + }; + } +} |