Source code
Revision control
Copy as Markdown
Other Tools
/* 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
#![cfg_attr(feature = "nightly", feature(proc_macro_expand))]
#![warn(rust_2018_idioms, unused_qualifications)]
//! Macros for `uniffi`.
#[cfg(feature = "trybuild")]
use camino::Utf8Path;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, Ident, LitStr, Path, Token,
};
mod custom;
mod default;
mod derive;
mod enum_;
mod error;
mod export;
mod ffiops;
mod fnsig;
mod object;
mod record;
mod setup_scaffolding;
mod test;
mod util;
use self::{
derive::DeriveOptions, enum_::expand_enum, error::expand_error, export::expand_export,
object::expand_object, record::expand_record,
};
struct CustomTypeInfo {
ident: Ident,
builtin: Path,
}
impl Parse for CustomTypeInfo {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let ident = input.parse()?;
input.parse::<Token![,]>()?;
let builtin = input.parse()?;
Ok(Self { ident, builtin })
}
}
/// 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)
}
/// Top-level initialization macro
///
/// The optional namespace argument is only used by the scaffolding templates to pass in the
/// CI namespace.
#[proc_macro]
pub fn setup_scaffolding(tokens: TokenStream) -> TokenStream {
let namespace = match syn::parse_macro_input!(tokens as Option<LitStr>) {
Some(lit_str) => lit_str.value(),
None => match util::mod_path() {
Ok(v) => v,
Err(e) => return e.into_compile_error().into(),
},
};
setup_scaffolding::setup_scaffolding(namespace)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_attribute]
pub fn export(attr_args: TokenStream, input: TokenStream) -> TokenStream {
do_export(attr_args, input, false)
}
fn do_export(attr_args: TokenStream, input: TokenStream, udl_mode: bool) -> TokenStream {
let copied_input = (!udl_mode).then(|| proc_macro2::TokenStream::from(input.clone()));
let gen_output = || {
let item = syn::parse(input)?;
expand_export(item, attr_args, udl_mode)
};
let output = gen_output().unwrap_or_else(syn::Error::into_compile_error);
quote! {
#copied_input
#output
}
.into()
}
#[proc_macro_derive(Record, attributes(uniffi))]
pub fn derive_record(input: TokenStream) -> TokenStream {
expand_record(parse_macro_input!(input), DeriveOptions::default())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(Enum)]
pub fn derive_enum(input: TokenStream) -> TokenStream {
expand_enum(parse_macro_input!(input), DeriveOptions::default())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(Object)]
pub fn derive_object(input: TokenStream) -> TokenStream {
expand_object(parse_macro_input!(input), DeriveOptions::default())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(Error, attributes(uniffi))]
pub fn derive_error(input: TokenStream) -> TokenStream {
expand_error(parse_macro_input!(input), DeriveOptions::default())
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
/// Generate the `FfiConverter` implementation for a Custom Type - ie,
/// for a `<T>` which implements `UniffiCustomTypeConverter`.
#[proc_macro]
pub fn custom_type(tokens: TokenStream) -> TokenStream {
let input: CustomTypeInfo = syn::parse_macro_input!(tokens);
custom::expand_ffi_converter_custom_type(&input.ident, &input.builtin, true)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
/// Generate the `FfiConverter` and the `UniffiCustomTypeConverter` implementations for a
/// Custom Type - ie, for a `<T>` which implements `UniffiCustomTypeConverter` via the
/// newtype idiom.
#[proc_macro]
pub fn custom_newtype(tokens: TokenStream) -> TokenStream {
let input: CustomTypeInfo = syn::parse_macro_input!(tokens);
custom::expand_ffi_converter_custom_newtype(&input.ident, &input.builtin, true)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
// Derive items for UDL mode
//
// The Askama templates generate placeholder items wrapped with the `#[udl_derive(<kind>)]`
// attribute. The macro code then generates derived items based on the input. This system ensures
// that the same code path is used for UDL-based code and proc-macros.
//
// # Differences between UDL-mode and normal mode
//
// ## Metadata symbols / checksum functions
//
// In UDL mode, we don't export the static metadata symbols or generate the checksum
// functions. This could be changed, but there doesn't seem to be much benefit at this point.
//
// ## The FfiConverter<UT> parameter
//
// In UDL-mode, we only implement `FfiConverter` for the local tag (`FfiConverter<crate::UniFfiTag>`)
//
// The reason for this split is remote types, i.e. types defined in remote crates that we
// don't control and therefore can't define a blanket impl on because of the orphan rules.
//
// With UDL, we handle this by only implementing `FfiConverter<crate::UniFfiTag>` for the
// type. This gets around the orphan rules since a local type is in the trait, but requires
// a `uniffi::ffi_converter_forward!` call if the type is used in a second local crate (an
// External typedef). This is natural for UDL-based generation, since you always need to
// define the external type in the UDL file.
//
// With proc-macros this system isn't so natural. Instead, we create a blanket implementation
// for all UT and support for remote types is still TODO.
#[doc(hidden)]
#[proc_macro_attribute]
pub fn udl_derive(attrs: TokenStream, input: TokenStream) -> TokenStream {
derive::expand_derive(
parse_macro_input!(attrs),
parse_macro_input!(input),
DeriveOptions::udl_derive(),
)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
// Generate export items for UDL mode
//
// This works similarly to `udl_derive`, but for #[export].
#[doc(hidden)]
#[proc_macro_attribute]
pub fn export_for_udl(attrs: TokenStream, input: TokenStream) -> TokenStream {
do_export(attrs, input, true)
}
/// 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(udl_stem: TokenStream) -> TokenStream {
let udl_stem = syn::parse_macro_input!(udl_stem as 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 {
let toml_path = match util::manifest_path() {
Ok(path) => path.display().to_string(),
Err(_) => {
return quote! {
compile_error!("This macro assumes the crate has a build.rs script, but $OUT_DIR is not present");
}.into();
}
};
quote! {
// FIXME(HACK):
// Include the `Cargo.toml` file into the build.
// That way cargo tracks the file and other tools relying on file
// tracking see it as well.
// In the future we should handle that by using the `track_path::path` API,
#[allow(dead_code)]
mod __unused {
const _: &[u8] = include_bytes!(#toml_path);
}
include!(concat!(env!("OUT_DIR"), "/", #udl_stem, ".uniffi.rs"));
}
}.into()
}
// Use a UniFFI types from dependent crates that uses UDL files
// See the derive_for_udl and export_for_udl section for a discussion of why this is needed.
#[proc_macro]
pub fn use_udl_record(tokens: TokenStream) -> TokenStream {
use_udl_simple_type(tokens)
}
#[proc_macro]
pub fn use_udl_enum(tokens: TokenStream) -> TokenStream {
use_udl_simple_type(tokens)
}
#[proc_macro]
pub fn use_udl_error(tokens: TokenStream) -> TokenStream {
use_udl_simple_type(tokens)
}
fn use_udl_simple_type(tokens: TokenStream) -> TokenStream {
let util::ExternalTypeItem {
crate_ident,
type_ident,
..
} = parse_macro_input!(tokens);
quote! {
::uniffi::ffi_converter_forward!(#type_ident, #crate_ident::UniFfiTag, crate::UniFfiTag);
}
.into()
}
#[proc_macro]
pub fn use_udl_object(tokens: TokenStream) -> TokenStream {
let util::ExternalTypeItem {
crate_ident,
type_ident,
..
} = parse_macro_input!(tokens);
quote! {
::uniffi::ffi_converter_arc_forward!(#type_ident, #crate_ident::UniFfiTag, crate::UniFfiTag);
}.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]
#[cfg(feature = "trybuild")]
pub fn generate_and_include_scaffolding(udl_file: TokenStream) -> TokenStream {
let udl_file = syn::parse_macro_input!(udl_file as 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 let Err(e) = uniffi_build::generate_scaffolding(udl_file_path) {
let err = format!("{e:#}");
quote! {
compile_error!(concat!("Failed to generate scaffolding from UDL file at ", #udl_file, ": ", #err));
}
} 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()
}
/// An attribute for constructors.
///
/// Constructors are in `impl` blocks which have a `#[uniffi::export]` attribute,
///
/// This exists so `#[uniffi::export]` can emit its input verbatim without
/// causing unexpected errors in the entire exported block.
/// This happens very often when the proc-macro is run on an incomplete
/// input by rust-analyzer while the developer is typing.
///
/// So much better to do nothing here then let the impl block find the attribute.
#[proc_macro_attribute]
pub fn constructor(_attrs: TokenStream, input: TokenStream) -> TokenStream {
input
}
/// An attribute for methods.
///
/// Everything above applies here too.
#[proc_macro_attribute]
pub fn method(_attrs: TokenStream, input: TokenStream) -> TokenStream {
input
}