summaryrefslogtreecommitdiffstats
path: root/third_party/rust/uniffi_macros/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/uniffi_macros/src/lib.rs')
-rw-r--r--third_party/rust/uniffi_macros/src/lib.rs238
1 files changed, 238 insertions, 0 deletions
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()
+}