1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 //! # Uniffi: easily build cross-platform software components in Rust
7 //! This is a highly-experimental crate for building cross-language software components
8 //! in Rust, based on things we've learned and patterns we've developed in the
9 //! [mozilla/application-services](https://github.com/mozilla/application-services) project.
11 //! The idea is to let you write your code once, in Rust, and then re-use it from many
12 //! other programming languages via Rust's C-compatible FFI layer and some automagically
13 //! generated binding code. If you think of it as a kind of [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen)
14 //! wannabe, with a clunkier developer experience but support for more target languages,
15 //! you'll be pretty close to the mark.
17 //! Currently supported target languages include Kotlin, Swift and Python.
21 //! To build a cross-language component using `uniffi`, follow these steps.
23 //! ### 1) Specify your Component Interface
25 //! Start by thinking about the interface you want to expose for use
26 //! from other languages. Use the Interface Definition Language to specify your interface
27 //! in a `.udl` file, where it can be processed by the tools from this crate.
28 //! For example you might define an interface like this:
31 //! namespace example {
35 //! dictionary MyData {
41 //! ### 2) Implement the Component Interface as a Rust crate
43 //! With the interface, defined, provide a corresponding implementation of that interface
44 //! as a standard-looking Rust crate, using functions and structs and so-on. For example
45 //! an implementation of the above Component Interface might look like this:
48 //! fn foo(bar: u32) -> u32 {
49 //! // TODO: a better example!
59 //! ### 3) Generate and include component scaffolding from the UDL file
61 //! First you will need to install `uniffi-bindgen` on your system using `cargo install uniffi_bindgen`.
62 //! Then add to your crate `uniffi_build` under `[build-dependencies]`.
63 //! Finally, add a `build.rs` script to your crate and have it call `uniffi_build::generate_scaffolding`
64 //! to process your `.udl` file. This will generate some Rust code to be included in the top-level source
65 //! code of your crate. If your UDL file is named `example.udl`, then your build script would call:
68 //! uniffi_build::generate_scaffolding("src/example.udl")
71 //! This would output a rust file named `example.uniffi.rs`, ready to be
72 //! included into the code of your rust crate like this:
75 //! include_scaffolding!("example");
78 //! ### 4) Generate foreign language bindings for the library
80 //! The `uniffi-bindgen` utility provides a command-line tool that can produce code to
81 //! consume the Rust library in any of several supported languages.
82 //! It is done by calling (in kotlin for example):
85 //! uniffi-bindgen --language kotlin ./src/example.udl
88 //! This will produce a file `example.kt` in the same directory as the .udl file, containing kotlin bindings
89 //! to load and use the compiled rust code via its C-compatible FFI.
92 #![warn(rust_2018_idioms, unused_qualifications)]
93 #![allow(unknown_lints)]
95 use anyhow::{anyhow, bail, Context, Result};
96 use camino::{Utf8Path, Utf8PathBuf};
97 use fs_err::{self as fs, File};
98 use serde::{de::DeserializeOwned, Deserialize, Serialize};
99 use std::io::prelude::*;
100 use std::io::ErrorKind;
101 use std::{collections::HashMap, process::Command};
106 pub mod library_mode;
107 pub mod macro_metadata;
110 use bindings::TargetLanguage;
111 pub use interface::ComponentInterface;
112 use scaffolding::RustScaffolding;
114 /// Trait for bindings configuration. Each bindings language defines one of these.
116 /// BindingsConfigs are initially loaded from `uniffi.toml` file. Then the trait methods are used
117 /// to fill in missing values.
118 pub trait BindingsConfig: DeserializeOwned {
119 /// Update missing values using the `ComponentInterface`
120 fn update_from_ci(&mut self, ci: &ComponentInterface);
122 /// Update missing values using the dylib file for the main crate, when in library mode.
124 /// cdylib_name will be the library filename without the leading `lib` and trailing extension
125 fn update_from_cdylib_name(&mut self, cdylib_name: &str);
127 /// Update missing values from config instances from dependent crates
129 /// config_map maps crate names to config instances. This is mostly used to set up external
131 fn update_from_dependency_configs(&mut self, config_map: HashMap<&str, &Self>);
134 /// Binding generator config with no members
135 #[derive(Clone, Debug, Deserialize, Hash, PartialEq, PartialOrd, Ord, Eq)]
136 pub struct EmptyBindingsConfig;
138 impl BindingsConfig for EmptyBindingsConfig {
139 fn update_from_ci(&mut self, _ci: &ComponentInterface) {}
140 fn update_from_cdylib_name(&mut self, _cdylib_name: &str) {}
141 fn update_from_dependency_configs(&mut self, _config_map: HashMap<&str, &Self>) {}
144 /// A trait representing a UniFFI Binding Generator
146 /// External crates that implement binding generators, should implement this type
147 /// and call the [`generate_external_bindings`] using a type that implements this trait.
148 pub trait BindingGenerator: Sized {
149 /// Handles configuring the bindings
150 type Config: BindingsConfig;
152 /// Writes the bindings to the output directory
155 /// - `ci`: A [`ComponentInterface`] representing the interface
156 /// - `config`: An instance of the [`BindingsConfig`] associated with this type
157 /// - `out_dir`: The path to where the binding generator should write the output bindings
160 ci: &ComponentInterface,
161 config: &Self::Config,
165 /// Check if `library_path` used by library mode is valid for this generator
166 fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()>;
169 struct BindingGeneratorDefault {
170 target_languages: Vec<TargetLanguage>,
171 try_format_code: bool,
174 impl BindingGenerator for BindingGeneratorDefault {
175 type Config = Config;
179 ci: &ComponentInterface,
180 config: &Self::Config,
183 for &language in &self.target_languages {
184 bindings::write_bindings(
189 self.try_format_code,
195 fn check_library_path(&self, library_path: &Utf8Path, cdylib_name: Option<&str>) -> Result<()> {
196 for &language in &self.target_languages {
197 if cdylib_name.is_none() && language != TargetLanguage::Swift {
198 bail!("Generate bindings for {language} requires a cdylib, but {library_path} was given");
205 /// Generate bindings for an external binding generator
206 /// Ideally, this should replace the [`generate_bindings`] function below
208 /// Implements an entry point for external binding generators.
209 /// The function does the following:
210 /// - It parses the `udl` in a [`ComponentInterface`]
211 /// - Parses the `uniffi.toml` and loads it into the type that implements [`BindingsConfig`]
212 /// - Creates an instance of [`BindingGenerator`], based on type argument `B`, and run [`BindingGenerator::write_bindings`] on it
215 /// - `binding_generator`: Type that implements BindingGenerator
216 /// - `udl_file`: The path to the UDL file
217 /// - `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.
218 /// - `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`
219 /// - `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`]
220 /// - `crate_name`: Override the default crate name that is guessed from UDL file path.
221 pub fn generate_external_bindings<T: BindingGenerator>(
222 binding_generator: T,
223 udl_file: impl AsRef<Utf8Path>,
224 config_file_override: Option<impl AsRef<Utf8Path>>,
225 out_dir_override: Option<impl AsRef<Utf8Path>>,
226 library_file: Option<impl AsRef<Utf8Path>>,
227 crate_name: Option<&str>,
229 let crate_name = crate_name
230 .map(|c| Ok(c.to_string()))
231 .unwrap_or_else(|| crate_name_from_cargo_toml(udl_file.as_ref()))?;
232 let mut component = parse_udl(udl_file.as_ref(), &crate_name)?;
233 if let Some(ref library_file) = library_file {
234 macro_metadata::add_to_ci_from_library(&mut component, library_file.as_ref())?;
236 let crate_root = &guess_crate_root(udl_file.as_ref()).context("Failed to guess crate root")?;
238 let config_file_override = config_file_override.as_ref().map(|p| p.as_ref());
241 let mut config = load_initial_config::<T::Config>(crate_root, config_file_override)?;
242 config.update_from_ci(&component);
243 if let Some(ref library_file) = library_file {
244 if let Some(cdylib_name) = crate::library_mode::calc_cdylib_name(library_file.as_ref())
246 config.update_from_cdylib_name(cdylib_name)
252 let out_dir = get_out_dir(
254 out_dir_override.as_ref().map(|p| p.as_ref()),
256 binding_generator.write_bindings(&component, &config, &out_dir)
259 // Generate the infrastructural Rust code for implementing the UDL interface,
260 // such as the `extern "C"` function definitions and record data types.
261 // Locates and parses Cargo.toml to determine the name of the crate.
262 pub fn generate_component_scaffolding(
264 out_dir_override: Option<&Utf8Path>,
267 let component = parse_udl(udl_file, &crate_name_from_cargo_toml(udl_file)?)?;
268 generate_component_scaffolding_inner(component, udl_file, out_dir_override, format_code)
271 // Generate the infrastructural Rust code for implementing the UDL interface,
272 // such as the `extern "C"` function definitions and record data types, using
273 // the specified crate name.
274 pub fn generate_component_scaffolding_for_crate(
277 out_dir_override: Option<&Utf8Path>,
280 let component = parse_udl(udl_file, crate_name)?;
281 generate_component_scaffolding_inner(component, udl_file, out_dir_override, format_code)
284 fn generate_component_scaffolding_inner(
285 component: ComponentInterface,
287 out_dir_override: Option<&Utf8Path>,
290 let file_stem = udl_file.file_stem().context("not a file")?;
291 let filename = format!("{file_stem}.uniffi.rs");
292 let out_path = get_out_dir(udl_file, out_dir_override)?.join(filename);
293 let mut f = File::create(&out_path)?;
294 write!(f, "{}", RustScaffolding::new(&component, file_stem))
295 .context("Failed to write output file")?;
297 format_code_with_rustfmt(&out_path)?;
302 // Generate the bindings in the target languages that call the scaffolding
304 pub fn generate_bindings(
306 config_file_override: Option<&Utf8Path>,
307 target_languages: Vec<TargetLanguage>,
308 out_dir_override: Option<&Utf8Path>,
309 library_file: Option<&Utf8Path>,
310 crate_name: Option<&str>,
311 try_format_code: bool,
313 generate_external_bindings(
314 BindingGeneratorDefault {
319 config_file_override,
326 pub fn print_repr(library_path: &Utf8Path) -> Result<()> {
327 let metadata = macro_metadata::extract_from_library(library_path)?;
328 println!("{metadata:#?}");
332 // Given the path to a UDL file, locate and parse the corresponding Cargo.toml to determine
333 // the library crate name.
334 // Note that this is largely a copy of code in uniffi_macros/src/util.rs, but sharing it
335 // isn't trivial and it's not particularly complicated so we've just copied it.
336 fn crate_name_from_cargo_toml(udl_file: &Utf8Path) -> Result<String> {
337 #[derive(Deserialize)]
344 #[derive(Deserialize)]
349 #[derive(Default, Deserialize)]
351 name: Option<String>,
354 let file = guess_crate_root(udl_file)?.join("Cargo.toml");
355 let cargo_toml_bytes =
356 fs::read(file).context("Can't find Cargo.toml to determine the crate name")?;
358 let cargo_toml = toml::from_slice::<CargoToml>(&cargo_toml_bytes)?;
360 let lib_crate_name = cargo_toml
363 .unwrap_or_else(|| cargo_toml.package.name.replace('-', "_"));
368 /// Guess the root directory of the crate from the path of its UDL file.
370 /// For now, we assume that the UDL file is in `./src/something.udl` relative
371 /// to the crate root. We might consider something more sophisticated in
373 pub fn guess_crate_root(udl_file: &Utf8Path) -> Result<&Utf8Path> {
374 let path_guess = udl_file
376 .context("UDL file has no parent folder!")?
378 .context("UDL file has no grand-parent folder!")?;
379 if !path_guess.join("Cargo.toml").is_file() {
380 bail!("UDL file does not appear to be inside a crate")
385 fn get_out_dir(udl_file: &Utf8Path, out_dir_override: Option<&Utf8Path>) -> Result<Utf8PathBuf> {
386 Ok(match out_dir_override {
388 // Create the directory if it doesn't exist yet.
389 fs::create_dir_all(s)?;
390 s.canonicalize_utf8().context("Unable to find out-dir")?
394 .context("File has no parent directory")?
399 fn parse_udl(udl_file: &Utf8Path, crate_name: &str) -> Result<ComponentInterface> {
400 let udl = fs::read_to_string(udl_file)
401 .with_context(|| format!("Failed to read UDL from {udl_file}"))?;
402 let group = uniffi_udl::parse_udl(&udl, crate_name)?;
403 ComponentInterface::from_metadata(group)
406 fn format_code_with_rustfmt(path: &Utf8Path) -> Result<()> {
407 let status = Command::new("rustfmt").arg(path).status().map_err(|e| {
408 let ctx = match e.kind() {
409 ErrorKind::NotFound => "formatting was requested, but rustfmt was not found",
410 _ => "unknown error when calling rustfmt",
412 anyhow!(e).context(ctx)
414 if !status.success() {
415 bail!("rustmt failed when formatting scaffolding. Note: --no-format can be used to skip formatting");
420 fn load_initial_config<Config: DeserializeOwned>(
421 crate_root: &Utf8Path,
422 config_file_override: Option<&Utf8Path>,
423 ) -> Result<Config> {
424 let path = match config_file_override {
425 Some(cfg) => Some(cfg.to_owned()),
426 None => crate_root.join("uniffi.toml").canonicalize_utf8().ok(),
428 let toml_config = match path {
430 let contents = fs::read_to_string(path).context("Failed to read config file")?;
431 toml::de::from_str(&contents)?
433 None => toml::Value::from(toml::value::Table::default()),
435 Ok(toml_config.try_into()?)
438 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
441 bindings: bindings::Config,
444 impl BindingsConfig for Config {
445 fn update_from_ci(&mut self, ci: &ComponentInterface) {
446 self.bindings.kotlin.update_from_ci(ci);
447 self.bindings.swift.update_from_ci(ci);
448 self.bindings.python.update_from_ci(ci);
449 self.bindings.ruby.update_from_ci(ci);
452 fn update_from_cdylib_name(&mut self, cdylib_name: &str) {
453 self.bindings.kotlin.update_from_cdylib_name(cdylib_name);
454 self.bindings.swift.update_from_cdylib_name(cdylib_name);
455 self.bindings.python.update_from_cdylib_name(cdylib_name);
456 self.bindings.ruby.update_from_cdylib_name(cdylib_name);
459 fn update_from_dependency_configs(&mut self, config_map: HashMap<&str, &Self>) {
460 self.bindings.kotlin.update_from_dependency_configs(
463 .map(|(key, config)| (*key, &config.bindings.kotlin))
466 self.bindings.swift.update_from_dependency_configs(
469 .map(|(key, config)| (*key, &config.bindings.swift))
472 self.bindings.python.update_from_dependency_configs(
475 .map(|(key, config)| (*key, &config.bindings.python))
478 self.bindings.ruby.update_from_dependency_configs(
481 .map(|(key, config)| (*key, &config.bindings.ruby))
488 // Include the askama config file into the build.
489 // That way cargo tracks the file and other tools relying on file tracking see it as well.
490 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1774585
491 // In the future askama should handle that itself by using the `track_path::path` API,
492 // see https://github.com/rust-lang/rust/pull/84029
495 const _: &[u8] = include_bytes!("../askama.toml");
503 fn test_guessing_of_crate_root_directory_from_udl_file() {
504 // When running this test, this will be the ./uniffi_bindgen directory.
505 let this_crate_root = Utf8PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
507 let example_crate_root = this_crate_root
509 .expect("should have a parent directory")
510 .join("examples/arithmetic");
512 guess_crate_root(&example_crate_root.join("src/arthmetic.udl")).unwrap(),
516 let not_a_crate_root = &this_crate_root.join("src/templates");
517 assert!(guess_crate_root(¬_a_crate_root.join("src/example.udl")).is_err());