summaryrefslogtreecommitdiffstats
path: root/third_party/rust/uniffi_bindgen/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/uniffi_bindgen/src/lib.rs')
-rw-r--r--third_party/rust/uniffi_bindgen/src/lib.rs483
1 files changed, 483 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..4959f0324a
--- /dev/null
+++ b/third_party/rust/uniffi_bindgen/src/lib.rs
@@ -0,0 +1,483 @@
+/* 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!(concat!(env!("OUT_DIR"), "/example.uniffi.rs"));
+//! ```
+//!
+//! ### 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)]
+
+const BINDGEN_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+use anyhow::{anyhow, bail, Context, Result};
+use camino::{Utf8Path, Utf8PathBuf};
+use fs_err::{self as fs, File};
+use serde::{Deserialize, Serialize};
+use std::io::prelude::*;
+use std::io::ErrorKind;
+use std::{collections::HashMap, env, process::Command, str::FromStr};
+
+pub mod backend;
+pub mod bindings;
+pub mod interface;
+pub mod macro_metadata;
+pub mod scaffolding;
+
+pub use interface::ComponentInterface;
+use scaffolding::RustScaffolding;
+
+/// A trait representing a Binding Generator Configuration
+///
+/// External crates that implement binding generators need to implement this trait and set it as
+/// the `BindingGenerator.config` associated type. `generate_external_bindings()` then uses it to
+/// generate the config that's passed to `BindingGenerator.write_bindings()`
+pub trait BindingGeneratorConfig: for<'de> Deserialize<'de> {
+ /// Get the entry for this config from the `bindings` table.
+ fn get_entry_from_bindings_table(bindings: &toml::Value) -> Option<toml::Value>;
+
+ /// Get default config values from the `ComponentInterface`
+ ///
+ /// These will replace missing entries in the bindings-specific config
+ fn get_config_defaults(ci: &ComponentInterface) -> Vec<(String, toml::Value)>;
+}
+
+fn load_bindings_config<BC: BindingGeneratorConfig>(
+ ci: &ComponentInterface,
+ crate_root: &Utf8Path,
+ config_file_override: Option<&Utf8Path>,
+) -> Result<BC> {
+ // Load the config from the TOML value, falling back to an empty map if it doesn't exist
+ let mut config_map: toml::value::Table =
+ match load_bindings_config_toml::<BC>(crate_root, config_file_override)? {
+ Some(value) => value
+ .try_into()
+ .context("Bindings config must be a TOML table")?,
+ None => toml::map::Map::new(),
+ };
+
+ // Update it with the defaults from the component interface
+ for (key, value) in BC::get_config_defaults(ci) {
+ config_map.entry(key).or_insert(value);
+ }
+
+ // Leverage serde to convert toml::Value into the config type
+ toml::Value::from(config_map)
+ .try_into()
+ .context("Generating bindings config from toml::Value")
+}
+
+/// Binding generator config with no members
+#[derive(Clone, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)]
+pub struct EmptyBindingGeneratorConfig;
+
+impl BindingGeneratorConfig for EmptyBindingGeneratorConfig {
+ fn get_entry_from_bindings_table(_bindings: &toml::Value) -> Option<toml::Value> {
+ None
+ }
+
+ fn get_config_defaults(_ci: &ComponentInterface) -> Vec<(String, toml::Value)> {
+ Vec::new()
+ }
+}
+
+// EmptyBindingGeneratorConfig is a unit struct, so the `derive(Deserialize)` implementation
+// expects a null value rather than the empty map that we pass it. So we need to implement
+// `Deserialize` ourselves.
+impl<'de> Deserialize<'de> for EmptyBindingGeneratorConfig {
+ fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Ok(EmptyBindingGeneratorConfig)
+ }
+}
+
+// Load the binding-specific config
+//
+// This function calculates the location of the config TOML file, parses it, and returns the result
+// as a toml::Value
+//
+// If there is an error parsing the file then Err will be returned. If the file is missing or the
+// entry for the bindings is missing, then Ok(None) will be returned.
+fn load_bindings_config_toml<BC: BindingGeneratorConfig>(
+ crate_root: &Utf8Path,
+ config_file_override: Option<&Utf8Path>,
+) -> Result<Option<toml::Value>> {
+ let config_path = match config_file_override {
+ Some(cfg) => cfg.to_owned(),
+ None => crate_root.join("uniffi.toml"),
+ };
+
+ if !config_path.exists() {
+ return Ok(None);
+ }
+
+ let contents = fs::read_to_string(&config_path)
+ .with_context(|| format!("Failed to read config file from {config_path}"))?;
+ let full_config = toml::Value::from_str(&contents)
+ .with_context(|| format!("Failed to parse config file {config_path}"))?;
+
+ Ok(full_config
+ .get("bindings")
+ .and_then(BC::get_entry_from_bindings_table))
+}
+
+/// 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 {
+ /// Associated type representing a the bindings-specifig configuration parsed from the
+ /// uniffi.toml
+ type Config: BindingGeneratorConfig;
+
+ /// Writes the bindings to the output directory
+ ///
+ /// # Arguments
+ /// - `ci`: A [`ComponentInterface`] representing the interface
+ /// - `config`: A instance of the BindingGeneratorConfig 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<()>;
+}
+
+/// 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 [`BindingGeneratorConfig`]
+/// - 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`
+pub fn generate_external_bindings(
+ binding_generator: impl BindingGenerator,
+ udl_file: impl AsRef<Utf8Path>,
+ config_file_override: Option<impl AsRef<Utf8Path>>,
+ out_dir_override: Option<impl AsRef<Utf8Path>>,
+) -> Result<()> {
+ let out_dir_override = out_dir_override.as_ref().map(|p| p.as_ref());
+ let config_file_override = config_file_override.as_ref().map(|p| p.as_ref());
+
+ let crate_root = guess_crate_root(udl_file.as_ref())?;
+ let out_dir = get_out_dir(udl_file.as_ref(), out_dir_override)?;
+ let component = parse_udl(udl_file.as_ref()).context("Error parsing UDL")?;
+ let bindings_config = load_bindings_config(&component, crate_root, config_file_override)?;
+ binding_generator.write_bindings(component, bindings_config, &out_dir)
+}
+
+// Generate the infrastructural Rust code for implementing the UDL interface,
+// such as the `extern "C"` function definitions and record data types.
+pub fn generate_component_scaffolding(
+ udl_file: &Utf8Path,
+ config_file_override: Option<&Utf8Path>,
+ out_dir_override: Option<&Utf8Path>,
+ format_code: bool,
+) -> Result<()> {
+ let component = parse_udl(udl_file)?;
+ let _config = get_config(
+ &component,
+ guess_crate_root(udl_file)?,
+ config_file_override,
+ );
+ 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)).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<&str>,
+ out_dir_override: Option<&Utf8Path>,
+ library_file: Option<&Utf8Path>,
+ try_format_code: bool,
+) -> Result<()> {
+ let mut component = parse_udl(udl_file)?;
+ if let Some(library_file) = library_file {
+ macro_metadata::add_to_ci_from_library(&mut component, library_file)?;
+ }
+ let crate_root = &guess_crate_root(udl_file)?;
+
+ let config = get_config(&component, crate_root, config_file_override)?;
+ let out_dir = get_out_dir(udl_file, out_dir_override)?;
+ for language in target_languages {
+ bindings::write_bindings(
+ &config.bindings,
+ &component,
+ &out_dir,
+ language.try_into()?,
+ try_format_code,
+ )?;
+ }
+
+ Ok(())
+}
+
+pub fn dump_json(library_path: &Utf8Path) -> Result<String> {
+ let metadata = macro_metadata::extract_from_library(library_path)?;
+ Ok(serde_json::to_string_pretty(&metadata)?)
+}
+
+pub fn print_json(library_path: &Utf8Path) -> Result<()> {
+ println!("{}", dump_json(library_path)?);
+ Ok(())
+}
+
+/// 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_config(
+ component: &ComponentInterface,
+ crate_root: &Utf8Path,
+ config_file_override: Option<&Utf8Path>,
+) -> Result<Config> {
+ let default_config: Config = component.into();
+
+ let config_file = match config_file_override {
+ Some(cfg) => Some(cfg.to_owned()),
+ None => crate_root.join("uniffi.toml").canonicalize_utf8().ok(),
+ };
+
+ match config_file {
+ Some(path) => {
+ let contents = fs::read_to_string(&path)
+ .with_context(|| format!("Failed to read config file from {path}"))?;
+ let loaded_config: Config = toml::de::from_str(&contents)
+ .with_context(|| format!("Failed to generate config from file {path}"))?;
+ Ok(loaded_config.merge_with(&default_config))
+ }
+ None => Ok(default_config),
+ }
+}
+
+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) -> Result<ComponentInterface> {
+ let udl = fs::read_to_string(udl_file)
+ .with_context(|| format!("Failed to read UDL from {udl_file}"))?;
+ ComponentInterface::from_webidl(&udl).context("Failed to parse UDL")
+}
+
+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(())
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+struct Config {
+ #[serde(default)]
+ bindings: bindings::Config,
+}
+
+impl From<&ComponentInterface> for Config {
+ fn from(ci: &ComponentInterface) -> Self {
+ Config {
+ bindings: ci.into(),
+ }
+ }
+}
+
+pub trait MergeWith {
+ fn merge_with(&self, other: &Self) -> Self;
+}
+
+impl MergeWith for Config {
+ fn merge_with(&self, other: &Self) -> Self {
+ Config {
+ bindings: self.bindings.merge_with(&other.bindings),
+ }
+ }
+}
+
+impl<T: Clone> MergeWith for Option<T> {
+ fn merge_with(&self, other: &Self) -> Self {
+ match (self, other) {
+ (Some(_), _) => self.clone(),
+ (None, Some(_)) => other.clone(),
+ (None, None) => None,
+ }
+ }
+}
+
+impl<V: Clone> MergeWith for HashMap<String, V> {
+ fn merge_with(&self, other: &Self) -> Self {
+ let mut merged = HashMap::new();
+ // Iterate through other first so our keys override theirs
+ for (key, value) in other.iter().chain(self) {
+ merged.insert(key.clone(), value.clone());
+ }
+ merged
+ }
+}
+
+// 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(&not_a_crate_root.join("./src/example.udl")).is_err());
+ }
+}