diff options
Diffstat (limited to 'third_party/rust/uniffi_bindgen/src/lib.rs')
-rw-r--r-- | third_party/rust/uniffi_bindgen/src/lib.rs | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/third_party/rust/uniffi_bindgen/src/lib.rs b/third_party/rust/uniffi_bindgen/src/lib.rs new file mode 100644 index 0000000000..019b24022f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/lib.rs @@ -0,0 +1,519 @@ +/* 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/. */ + +//! # Uniffi: easily build cross-platform software components in Rust +//! +//! This is a highly-experimental crate for building cross-language software components +//! in Rust, based on things we've learned and patterns we've developed in the +//! [mozilla/application-services](https://github.com/mozilla/application-services) project. +//! +//! The idea is to let you write your code once, in Rust, and then re-use it from many +//! other programming languages via Rust's C-compatible FFI layer and some automagically +//! generated binding code. If you think of it as a kind of [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) +//! wannabe, with a clunkier developer experience but support for more target languages, +//! you'll be pretty close to the mark. +//! +//! Currently supported target languages include Kotlin, Swift and Python. +//! +//! ## Usage +// +//! To build a cross-language component using `uniffi`, follow these steps. +//! +//! ### 1) Specify your Component Interface +//! +//! Start by thinking about the interface you want to expose for use +//! from other languages. Use the Interface Definition Language to specify your interface +//! in a `.udl` file, where it can be processed by the tools from this crate. +//! For example you might define an interface like this: +//! +//! ```text +//! namespace example { +//! u32 foo(u32 bar); +//! } +//! +//! dictionary MyData { +//! u32 num_foos; +//! bool has_a_bar; +//! } +//! ``` +//! +//! ### 2) Implement the Component Interface as a Rust crate +//! +//! With the interface, defined, provide a corresponding implementation of that interface +//! as a standard-looking Rust crate, using functions and structs and so-on. For example +//! an implementation of the above Component Interface might look like this: +//! +//! ```text +//! fn foo(bar: u32) -> u32 { +//! // TODO: a better example! +//! bar + 42 +//! } +//! +//! struct MyData { +//! num_foos: u32, +//! has_a_bar: bool +//! } +//! ``` +//! +//! ### 3) Generate and include component scaffolding from the UDL file +//! +//! First you will need to install `uniffi-bindgen` on your system using `cargo install uniffi_bindgen`. +//! Then add to your crate `uniffi_build` under `[build-dependencies]`. +//! Finally, add a `build.rs` script to your crate and have it call `uniffi_build::generate_scaffolding` +//! to process your `.udl` file. This will generate some Rust code to be included in the top-level source +//! code of your crate. If your UDL file is named `example.udl`, then your build script would call: +//! +//! ```text +//! uniffi_build::generate_scaffolding("src/example.udl") +//! ``` +//! +//! This would output a rust file named `example.uniffi.rs`, ready to be +//! included into the code of your rust crate like this: +//! +//! ```text +//! include_scaffolding!("example"); +//! ``` +//! +//! ### 4) Generate foreign language bindings for the library +//! +//! The `uniffi-bindgen` utility provides a command-line tool that can produce code to +//! consume the Rust library in any of several supported languages. +//! It is done by calling (in kotlin for example): +//! +//! ```text +//! uniffi-bindgen --language kotlin ./src/example.udl +//! ``` +//! +//! This will produce a file `example.kt` in the same directory as the .udl file, containing kotlin bindings +//! to load and use the compiled rust code via its C-compatible FFI. +//! + +#![warn(rust_2018_idioms, unused_qualifications)] +#![allow(unknown_lints)] + +use anyhow::{anyhow, bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use fs_err::{self as fs, File}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::io::prelude::*; +use std::io::ErrorKind; +use std::{collections::HashMap, process::Command}; + +pub mod backend; +pub mod bindings; +pub mod interface; +pub mod library_mode; +pub mod macro_metadata; +pub mod scaffolding; + +use bindings::TargetLanguage; +pub use interface::ComponentInterface; +use scaffolding::RustScaffolding; + +/// Trait for bindings configuration. Each bindings language defines one of these. +/// +/// BindingsConfigs are initially loaded from `uniffi.toml` file. Then the trait methods are used +/// to fill in missing values. +pub trait BindingsConfig: DeserializeOwned { + /// Update missing values using the `ComponentInterface` + fn update_from_ci(&mut self, ci: &ComponentInterface); + + /// Update missing values using the dylib file for the main crate, when in library mode. + /// + /// cdylib_name will be the library filename without the leading `lib` and trailing extension + fn update_from_cdylib_name(&mut self, cdylib_name: &str); + + /// Update missing values from config instances from dependent crates + /// + /// config_map maps crate names to config instances. This is mostly used to set up external + /// types. + fn update_from_dependency_configs(&mut self, config_map: HashMap<&str, &Self>); +} + +/// Binding generator config with no members +#[derive(Clone, Debug, Deserialize, Hash, PartialEq, PartialOrd, Ord, Eq)] +pub struct EmptyBindingsConfig; + +impl BindingsConfig for EmptyBindingsConfig { + fn update_from_ci(&mut self, _ci: &ComponentInterface) {} + fn update_from_cdylib_name(&mut self, _cdylib_name: &str) {} + fn update_from_dependency_configs(&mut self, _config_map: HashMap<&str, &Self>) {} +} + +/// A trait representing a UniFFI Binding Generator +/// +/// External crates that implement binding generators, should implement this type +/// and call the [`generate_external_bindings`] using a type that implements this trait. +pub trait BindingGenerator: Sized { + /// Handles configuring the bindings + type Config: BindingsConfig; + + /// Writes the bindings to the output directory + /// + /// # Arguments + /// - `ci`: A [`ComponentInterface`] representing the interface + /// - `config`: An instance of the [`BindingsConfig`] associated with this type + /// - `out_dir`: The path to where the binding generator should write the output bindings + fn write_bindings( + &self, + ci: &ComponentInterface, + config: &Self::Config, + out_dir: &Utf8Path, + ) -> Result<()>; + + /// Check if `library_path` used by library mode is valid for this generator + fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()>; +} + +struct BindingGeneratorDefault { + target_languages: Vec<TargetLanguage>, + try_format_code: bool, +} + +impl BindingGenerator for BindingGeneratorDefault { + type Config = Config; + + fn write_bindings( + &self, + ci: &ComponentInterface, + config: &Self::Config, + out_dir: &Utf8Path, + ) -> Result<()> { + for &language in &self.target_languages { + bindings::write_bindings( + &config.bindings, + ci, + out_dir, + language, + self.try_format_code, + )?; + } + Ok(()) + } + + fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()> { + for &language in &self.target_languages { + if cdylib_name.is_none() && language != TargetLanguage::Swift { + bail!("Generate bindings for {language} requires a cdylib, but {library_path} was given"); + } + } + Ok(()) + } +} + +/// Generate bindings for an external binding generator +/// Ideally, this should replace the [`generate_bindings`] function below +/// +/// Implements an entry point for external binding generators. +/// The function does the following: +/// - It parses the `udl` in a [`ComponentInterface`] +/// - Parses the `uniffi.toml` and loads it into the type that implements [`BindingsConfig`] +/// - Creates an instance of [`BindingGenerator`], based on type argument `B`, and run [`BindingGenerator::write_bindings`] on it +/// +/// # Arguments +/// - `binding_generator`: Type that implements BindingGenerator +/// - `udl_file`: The path to the UDL file +/// - `config_file_override`: The path to the configuration toml file, most likely called `uniffi.toml`. If [`None`], the function will try to guess based on the crate's root. +/// - `out_dir_override`: The path to write the bindings to. If [`None`], it will be the path to the parent directory of the `udl_file` +/// - `library_file`: The path to a dynamic library to attempt to extract the definitions from and extend the component interface with. No extensions to component interface occur if it's [`None`] +/// - `crate_name`: Override the default crate name that is guessed from UDL file path. +pub fn generate_external_bindings<T: BindingGenerator>( + binding_generator: T, + udl_file: impl AsRef<Utf8Path>, + config_file_override: Option<impl AsRef<Utf8Path>>, + out_dir_override: Option<impl AsRef<Utf8Path>>, + library_file: Option<impl AsRef<Utf8Path>>, + crate_name: Option<&str>, +) -> Result<()> { + let crate_name = crate_name + .map(|c| Ok(c.to_string())) + .unwrap_or_else(|| crate_name_from_cargo_toml(udl_file.as_ref()))?; + let mut component = parse_udl(udl_file.as_ref(), &crate_name)?; + if let Some(ref library_file) = library_file { + macro_metadata::add_to_ci_from_library(&mut component, library_file.as_ref())?; + } + let crate_root = &guess_crate_root(udl_file.as_ref()).context("Failed to guess crate root")?; + + let config_file_override = config_file_override.as_ref().map(|p| p.as_ref()); + + let config = { + let mut config = load_initial_config::<T::Config>(crate_root, config_file_override)?; + config.update_from_ci(&component); + if let Some(ref library_file) = library_file { + if let Some(cdylib_name) = crate::library_mode::calc_cdylib_name(library_file.as_ref()) + { + config.update_from_cdylib_name(cdylib_name) + } + }; + config + }; + + let out_dir = get_out_dir( + udl_file.as_ref(), + out_dir_override.as_ref().map(|p| p.as_ref()), + )?; + binding_generator.write_bindings(&component, &config, &out_dir) +} + +// Generate the infrastructural Rust code for implementing the UDL interface, +// such as the `extern "C"` function definitions and record data types. +// Locates and parses Cargo.toml to determine the name of the crate. +pub fn generate_component_scaffolding( + udl_file: &Utf8Path, + out_dir_override: Option<&Utf8Path>, + format_code: bool, +) -> Result<()> { + let component = parse_udl(udl_file, &crate_name_from_cargo_toml(udl_file)?)?; + generate_component_scaffolding_inner(component, udl_file, out_dir_override, format_code) +} + +// Generate the infrastructural Rust code for implementing the UDL interface, +// such as the `extern "C"` function definitions and record data types, using +// the specified crate name. +pub fn generate_component_scaffolding_for_crate( + udl_file: &Utf8Path, + crate_name: &str, + out_dir_override: Option<&Utf8Path>, + format_code: bool, +) -> Result<()> { + let component = parse_udl(udl_file, crate_name)?; + generate_component_scaffolding_inner(component, udl_file, out_dir_override, format_code) +} + +fn generate_component_scaffolding_inner( + component: ComponentInterface, + udl_file: &Utf8Path, + out_dir_override: Option<&Utf8Path>, + format_code: bool, +) -> Result<()> { + let file_stem = udl_file.file_stem().context("not a file")?; + let filename = format!("{file_stem}.uniffi.rs"); + let out_path = get_out_dir(udl_file, out_dir_override)?.join(filename); + let mut f = File::create(&out_path)?; + write!(f, "{}", RustScaffolding::new(&component, file_stem)) + .context("Failed to write output file")?; + if format_code { + format_code_with_rustfmt(&out_path)?; + } + Ok(()) +} + +// Generate the bindings in the target languages that call the scaffolding +// Rust code. +pub fn generate_bindings( + udl_file: &Utf8Path, + config_file_override: Option<&Utf8Path>, + target_languages: Vec<TargetLanguage>, + out_dir_override: Option<&Utf8Path>, + library_file: Option<&Utf8Path>, + crate_name: Option<&str>, + try_format_code: bool, +) -> Result<()> { + generate_external_bindings( + BindingGeneratorDefault { + target_languages, + try_format_code, + }, + udl_file, + config_file_override, + out_dir_override, + library_file, + crate_name, + ) +} + +pub fn print_repr(library_path: &Utf8Path) -> Result<()> { + let metadata = macro_metadata::extract_from_library(library_path)?; + println!("{metadata:#?}"); + Ok(()) +} + +// Given the path to a UDL file, locate and parse the corresponding Cargo.toml to determine +// the library crate name. +// Note that this is largely a copy of code in uniffi_macros/src/util.rs, but sharing it +// isn't trivial and it's not particularly complicated so we've just copied it. +fn crate_name_from_cargo_toml(udl_file: &Utf8Path) -> Result<String> { + #[derive(Deserialize)] + struct CargoToml { + package: Package, + #[serde(default)] + lib: Lib, + } + + #[derive(Deserialize)] + struct Package { + name: String, + } + + #[derive(Default, Deserialize)] + struct Lib { + name: Option<String>, + } + + let file = guess_crate_root(udl_file)?.join("Cargo.toml"); + let cargo_toml_bytes = + fs::read(file).context("Can't find Cargo.toml to determine the crate name")?; + + let cargo_toml = toml::from_slice::<CargoToml>(&cargo_toml_bytes)?; + + let lib_crate_name = cargo_toml + .lib + .name + .unwrap_or_else(|| cargo_toml.package.name.replace('-', "_")); + + Ok(lib_crate_name) +} + +/// Guess the root directory of the crate from the path of its UDL file. +/// +/// For now, we assume that the UDL file is in `./src/something.udl` relative +/// to the crate root. We might consider something more sophisticated in +/// future. +pub fn guess_crate_root(udl_file: &Utf8Path) -> Result<&Utf8Path> { + let path_guess = udl_file + .parent() + .context("UDL file has no parent folder!")? + .parent() + .context("UDL file has no grand-parent folder!")?; + if !path_guess.join("Cargo.toml").is_file() { + bail!("UDL file does not appear to be inside a crate") + } + Ok(path_guess) +} + +fn get_out_dir(udl_file: &Utf8Path, out_dir_override: Option<&Utf8Path>) -> Result<Utf8PathBuf> { + Ok(match out_dir_override { + Some(s) => { + // Create the directory if it doesn't exist yet. + fs::create_dir_all(s)?; + s.canonicalize_utf8().context("Unable to find out-dir")? + } + None => udl_file + .parent() + .context("File has no parent directory")? + .to_owned(), + }) +} + +fn parse_udl(udl_file: &Utf8Path, crate_name: &str) -> Result<ComponentInterface> { + let udl = fs::read_to_string(udl_file) + .with_context(|| format!("Failed to read UDL from {udl_file}"))?; + let group = uniffi_udl::parse_udl(&udl, crate_name)?; + ComponentInterface::from_metadata(group) +} + +fn format_code_with_rustfmt(path: &Utf8Path) -> Result<()> { + let status = Command::new("rustfmt").arg(path).status().map_err(|e| { + let ctx = match e.kind() { + ErrorKind::NotFound => "formatting was requested, but rustfmt was not found", + _ => "unknown error when calling rustfmt", + }; + anyhow!(e).context(ctx) + })?; + if !status.success() { + bail!("rustmt failed when formatting scaffolding. Note: --no-format can be used to skip formatting"); + } + Ok(()) +} + +fn load_initial_config<Config: DeserializeOwned>( + crate_root: &Utf8Path, + config_file_override: Option<&Utf8Path>, +) -> Result<Config> { + let path = match config_file_override { + Some(cfg) => Some(cfg.to_owned()), + None => crate_root.join("uniffi.toml").canonicalize_utf8().ok(), + }; + let toml_config = match path { + Some(path) => { + let contents = fs::read_to_string(path).context("Failed to read config file")?; + toml::de::from_str(&contents)? + } + None => toml::Value::from(toml::value::Table::default()), + }; + Ok(toml_config.try_into()?) +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + #[serde(default)] + bindings: bindings::Config, +} + +impl BindingsConfig for Config { + fn update_from_ci(&mut self, ci: &ComponentInterface) { + self.bindings.kotlin.update_from_ci(ci); + self.bindings.swift.update_from_ci(ci); + self.bindings.python.update_from_ci(ci); + self.bindings.ruby.update_from_ci(ci); + } + + fn update_from_cdylib_name(&mut self, cdylib_name: &str) { + self.bindings.kotlin.update_from_cdylib_name(cdylib_name); + self.bindings.swift.update_from_cdylib_name(cdylib_name); + self.bindings.python.update_from_cdylib_name(cdylib_name); + self.bindings.ruby.update_from_cdylib_name(cdylib_name); + } + + fn update_from_dependency_configs(&mut self, config_map: HashMap<&str, &Self>) { + self.bindings.kotlin.update_from_dependency_configs( + config_map + .iter() + .map(|(key, config)| (*key, &config.bindings.kotlin)) + .collect(), + ); + self.bindings.swift.update_from_dependency_configs( + config_map + .iter() + .map(|(key, config)| (*key, &config.bindings.swift)) + .collect(), + ); + self.bindings.python.update_from_dependency_configs( + config_map + .iter() + .map(|(key, config)| (*key, &config.bindings.python)) + .collect(), + ); + self.bindings.ruby.update_from_dependency_configs( + config_map + .iter() + .map(|(key, config)| (*key, &config.bindings.ruby)) + .collect(), + ); + } +} + +// FIXME(HACK): +// Include the askama config file into the build. +// That way cargo tracks the file and other tools relying on file tracking see it as well. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1774585 +// In the future askama should handle that itself by using the `track_path::path` API, +// see https://github.com/rust-lang/rust/pull/84029 +#[allow(dead_code)] +mod __unused { + const _: &[u8] = include_bytes!("../askama.toml"); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_guessing_of_crate_root_directory_from_udl_file() { + // When running this test, this will be the ./uniffi_bindgen directory. + let this_crate_root = Utf8PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + + let example_crate_root = this_crate_root + .parent() + .expect("should have a parent directory") + .join("examples/arithmetic"); + assert_eq!( + guess_crate_root(&example_crate_root.join("src/arthmetic.udl")).unwrap(), + example_crate_root + ); + + let not_a_crate_root = &this_crate_root.join("src/templates"); + assert!(guess_crate_root(¬_a_crate_root.join("src/example.udl")).is_err()); + } +} |