From 1eeb9de4e1f0938946b4956e51b1ec10ded57c3e Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Tue, 9 Sep 2025 16:27:38 -0400 Subject: [PATCH] Adding BindgenLoader This is a new way for binding generators to load the CIs, Configs, metadata, etc. I'm hoping that this can replace the `generate_bindings` and `generate_external_bindings` functions, as well some other functions like `library_mode::find_components`. The goal for the new design is to let bindings generators drive the process instead of a function like `generate_external_bindings`. The advantage of this is that it's easier to customize and we don't need to keep adding new hook methods. It also feels simpler overall to me. See https://github.com/mozilla/uniffi-rs/pull/2640 for a discussion of this. If we adopt this new system, then I think we can use it to remove a bunch of duplicate code. We can keep around the old functions for backwards-compatibility, but I think they can just be wrappers around `BindgenLoader`. Also, we should figure out a nice way to hook this up to the pipeline code. Made `uniffi-bindgen-swift` use the new system. This was mostly to test the code and to serve as an example, but a nice side-benefit is that `uniffi-bindgen-swift` now can handle UDL files. I also added a `uniffi-bindgen-swift` binary file to go with the `uniffi-bindgen` binary. --- uniffi/Cargo.toml | 6 + uniffi/src/cli/swift.rs | 7 +- uniffi/uniffi-bindgen-swift.rs | 9 + uniffi_bindgen/src/bindings/swift/mod.rs | 50 +++--- uniffi_bindgen/src/lib.rs | 3 + uniffi_bindgen/src/library_mode.rs | 25 ++- uniffi_bindgen/src/loader.rs | 209 +++++++++++++++++++++++ uniffi_bindgen/src/macro_metadata/mod.rs | 2 +- uniffi_meta/src/group.rs | 2 +- uniffi_meta/src/lib.rs | 2 +- 10 files changed, 279 insertions(+), 36 deletions(-) create mode 100644 uniffi/uniffi-bindgen-swift.rs create mode 100644 uniffi_bindgen/src/loader.rs diff --git a/uniffi/Cargo.toml b/uniffi/Cargo.toml index 5a67b71a97..df90d05bf9 100644 --- a/uniffi/Cargo.toml +++ b/uniffi/Cargo.toml @@ -68,3 +68,9 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" required-features = ["cli"] doc = false + +[[bin]] +name = "uniffi-bindgen-swift" +path = "uniffi-bindgen-swift.rs" +required-features = ["cli"] +doc = false diff --git a/uniffi/src/cli/swift.rs b/uniffi/src/cli/swift.rs index 4eb6683ec8..90486d49fa 100644 --- a/uniffi/src/cli/swift.rs +++ b/uniffi/src/cli/swift.rs @@ -13,8 +13,9 @@ use uniffi_bindgen::bindings::{generate_swift_bindings, SwiftBindingsOptions}; struct Cli { #[command(flatten)] kinds: Kinds, - /// Library path to generate bindings for - library_path: Utf8PathBuf, + #[clap(name = "PATH_TO_LIBRARY_OR_UDL")] + /// UDL File / path to generate bindings for + source: Utf8PathBuf, /// Directory to generate files in out_dir: Utf8PathBuf, /// Generate a XCFramework-compatible modulemap @@ -65,7 +66,7 @@ impl From for SwiftBindingsOptions { generate_swift_sources: cli.kinds.swift_sources, generate_headers: cli.kinds.headers, generate_modulemap: cli.kinds.modulemap, - library_path: cli.library_path, + source: cli.source, out_dir: cli.out_dir, xcframework: cli.xcframework, module_name: cli.module_name, diff --git a/uniffi/uniffi-bindgen-swift.rs b/uniffi/uniffi-bindgen-swift.rs new file mode 100644 index 0000000000..a73e68c286 --- /dev/null +++ b/uniffi/uniffi-bindgen-swift.rs @@ -0,0 +1,9 @@ +/* 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/. */ + +use uniffi::uniffi_bindgen_swift; + +fn main() { + uniffi_bindgen_swift(); +} diff --git a/uniffi_bindgen/src/bindings/swift/mod.rs b/uniffi_bindgen/src/bindings/swift/mod.rs index ba112c2215..e946e0c2c2 100644 --- a/uniffi_bindgen/src/bindings/swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/mod.rs @@ -29,8 +29,8 @@ //! * How to read from and write into a byte buffer. //! -use crate::{BindingGenerator, Component, GenerationSettings}; -use anyhow::{Context, Result}; +use crate::{BindgenLoader, BindingGenerator, Component, ComponentInterface, GenerationSettings}; +use anyhow::Result; use camino::Utf8PathBuf; use fs_err as fs; use std::process::Command; @@ -147,6 +147,7 @@ pub fn generate_swift_bindings(options: SwiftBindingsOptions) -> Result<()> { #[cfg(feature = "cargo-metadata")] let config_supplier = { use crate::cargo_metadata::CrateConfigSupplier; + use anyhow::Context; let mut cmd = cargo_metadata::MetadataCommand::new(); if options.metadata_no_deps { cmd.no_deps(); @@ -159,17 +160,10 @@ pub fn generate_swift_bindings(options: SwiftBindingsOptions) -> Result<()> { fs::create_dir_all(&options.out_dir)?; - let mut components = - crate::library_mode::find_components(&options.library_path, &config_supplier)? - // map the TOML configs into a our Config struct - .into_iter() - .map(|Component { ci, config }| { - let config = SwiftBindingGenerator.new_config(&config.into())?; - Ok(Component { ci, config }) - }) - .collect::>>()?; - SwiftBindingGenerator - .update_component_configs(&GenerationSettings::default(), &mut components)?; + let loader = BindgenLoader::new(&config_supplier); + let metadata = loader.load_metadata(&options.source)?; + let cis = loader.load_cis(metadata)?; + let components = loader.load_components(cis, parse_config)?; for Component { ci, config } in &components { if options.generate_swift_sources { @@ -185,24 +179,15 @@ pub fn generate_swift_bindings(options: SwiftBindingsOptions) -> Result<()> { } } - // find the library name by stripping the extension and leading `lib` from the library path - let library_name = { - let stem = options - .library_path - .file_stem() - .with_context(|| format!("Invalid library path {}", options.library_path))?; - match stem.strip_prefix("lib") { - Some(name) => name, - None => stem, - } - }; + // Derive the default module_name/modulemap_filename from the source filename. + let source_basename = loader.source_basename(&options.source); let module_name = options .module_name - .unwrap_or_else(|| library_name.to_string()); + .unwrap_or_else(|| source_basename.to_string()); let modulemap_filename = options .modulemap_filename - .unwrap_or_else(|| format!("{library_name}.modulemap")); + .unwrap_or_else(|| format!("{source_basename}.modulemap")); if options.generate_modulemap { let mut header_filenames: Vec<_> = components @@ -223,12 +208,23 @@ pub fn generate_swift_bindings(options: SwiftBindingsOptions) -> Result<()> { Ok(()) } +fn parse_config(ci: &ComponentInterface, root_toml: toml::Value) -> Result { + let mut config: Config = match root_toml.get("bindings").and_then(|b| b.get("swift")) { + Some(v) => v.clone().try_into()?, + None => Default::default(), + }; + config + .module_name + .get_or_insert_with(|| ci.namespace().into()); + Ok(config) +} + #[derive(Debug, Default)] pub struct SwiftBindingsOptions { pub generate_swift_sources: bool, pub generate_headers: bool, pub generate_modulemap: bool, - pub library_path: Utf8PathBuf, + pub source: Utf8PathBuf, pub out_dir: Utf8PathBuf, pub xcframework: bool, pub module_name: Option, diff --git a/uniffi_bindgen/src/lib.rs b/uniffi_bindgen/src/lib.rs index 9a553ad36e..639683a2d9 100644 --- a/uniffi_bindgen/src/lib.rs +++ b/uniffi_bindgen/src/lib.rs @@ -104,6 +104,7 @@ use std::process::Command; pub mod bindings; pub mod interface; pub mod library_mode; +mod loader; pub mod macro_metadata; pub mod pipeline; pub mod scaffolding; @@ -120,6 +121,8 @@ pub use library_mode::find_components; use scaffolding::RustScaffolding; use uniffi_meta::Type; +pub use loader::BindgenLoader; + /// The options used when creating bindings. Named such /// it doesn't cause confusion that it's settings specific to /// the generator itself. diff --git a/uniffi_bindgen/src/library_mode.rs b/uniffi_bindgen/src/library_mode.rs index 4b09b4aa9c..e6e0eef854 100644 --- a/uniffi_bindgen/src/library_mode.rs +++ b/uniffi_bindgen/src/library_mode.rs @@ -107,10 +107,10 @@ pub fn calc_cdylib_name(library_path: &Utf8Path) -> Option<&str> { /// calls. /// /// `config_supplier` is used to find UDL files on disk and load config data. -pub fn find_components( +pub fn find_cis( library_path: &Utf8Path, config_supplier: &dyn BindgenCrateConfigSupplier, -) -> Result>> { +) -> Result> { let items = macro_metadata::extract_from_library(library_path)?; let mut metadata_groups = create_metadata_groups(&items); group_metadata(&mut metadata_groups, items)?; @@ -137,10 +137,29 @@ pub fn find_components( let crate_name = &group.namespace.crate_name; let mut ci = ComponentInterface::new(crate_name); ci.add_metadata(group)?; + ci.set_crate_to_namespace_map(crate_to_namespace_map.clone()); + Ok(ci) + }) + .collect() +} + +/// Find UniFFI components from a shared library file +/// +/// This method inspects the library file and creates [ComponentInterface] instances for each +/// component used to build it. It parses the UDL files from `uniffi::include_scaffolding!` macro +/// calls. +/// +/// `config_supplier` is used to find UDL files on disk and load config data. +pub fn find_components( + library_path: &Utf8Path, + config_supplier: &dyn BindgenCrateConfigSupplier, +) -> Result>> { + find_cis(library_path, config_supplier)? + .into_iter() + .map(|ci| { let config = config_supplier .get_toml(ci.crate_name())? .unwrap_or_default(); - ci.set_crate_to_namespace_map(crate_to_namespace_map.clone()); Ok(Component { ci, config }) }) .collect() diff --git a/uniffi_bindgen/src/loader.rs b/uniffi_bindgen/src/loader.rs new file mode 100644 index 0000000000..788db42390 --- /dev/null +++ b/uniffi_bindgen/src/loader.rs @@ -0,0 +1,209 @@ +/* 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/. */ + +use std::{ + collections::{BTreeMap, HashMap}, + fs, +}; + +use anyhow::bail; +use camino::Utf8Path; +use uniffi_meta::{ + create_metadata_groups, group_metadata, Metadata, MetadataGroup, MetadataGroupMap, + NamespaceMetadata, +}; + +use crate::{ + crate_name_from_cargo_toml, macro_metadata, BindgenCrateConfigSupplier, Component, + ComponentInterface, Result, +}; + +/// Load metadata, component interfaces, configuration, etc. for binding generators. +/// +/// Bindings generators use this to load all of the inputs they need to render their code. +pub struct BindgenLoader<'a> { + config_supplier: &'a dyn BindgenCrateConfigSupplier, +} + +impl<'config> BindgenLoader<'config> { + pub fn new(config_supplier: &'config dyn BindgenCrateConfigSupplier) -> Self { + Self { config_supplier } + } + + /// Load UniFFI metadata + /// + /// The metadata describes the exported interface. + /// + /// `source_path` can be: + /// - A library file (.so, .dylib, .a, etc). Metadata will be loaded from the symbol table. + /// - A UDL file. The UDL will be parsed and converted into metadata. + pub fn load_metadata(&self, source_path: &Utf8Path) -> Result { + self.load_metadata_specialized(source_path, |_, _| Ok(None)) + } + + /// Load UniFFI metadata with a specialized metadata parser + /// + /// When loading metadata from a library, the passed-in specialized parser will be used to + /// parse the data. The library path and library contents will be passed. If the parser + /// returns `Ok(Some(metadata))` then this metadata will be used. If it returns `Ok(None)` than + /// the default parsing will be used. + /// + /// Use this function when you want to support specialized library formats. + pub fn load_metadata_specialized

( + &self, + source_path: &Utf8Path, + specialized_parser: P, + ) -> Result + where + P: FnOnce(&Utf8Path, &[u8]) -> Result>>, + { + match source_path.extension() { + Some(ext) if ext.to_lowercase() == "udl" => { + let crate_name = crate_name_from_cargo_toml(source_path)?; + let group = uniffi_udl::parse_udl(&fs::read_to_string(source_path)?, &crate_name)?; + Ok(HashMap::from([(crate_name, group)])) + } + _ => { + let data = fs::read(source_path)?; + let items = match specialized_parser(source_path, &data)? { + Some(items) => items, + None => macro_metadata::extract_from_bytes(&data)?, + }; + let mut metadata_groups = create_metadata_groups(&items); + group_metadata(&mut metadata_groups, items)?; + + for group in metadata_groups.values_mut() { + let crate_name = group.namespace.crate_name.clone(); + if let Some(udl_group) = self.load_udl_metadata(group, &crate_name)? { + let mut udl_items = udl_group.items.into_iter().collect(); + group.items.append(&mut udl_items); + if group.namespace_docstring.is_none() { + group.namespace_docstring = udl_group.namespace_docstring; + } + }; + } + Ok(metadata_groups) + } + } + } + + /// Load a [ComponentInterface] list + /// + /// This converts the metadata into `ComponentInterface` instances, which contains additional + /// derived information about the interface, like FFI functions signatures. + pub fn load_cis(&self, metadata: MetadataGroupMap) -> Result> { + let crate_to_namespace_map: BTreeMap = metadata + .iter() + .map(|(k, v)| (k.clone(), v.namespace.clone())) + .collect(); + + let mut ci_list = metadata + .into_values() + .map(|group| { + let crate_name = &group.namespace.crate_name; + let mut ci = ComponentInterface::new(crate_name); + ci.add_metadata(group)?; + ci.set_crate_to_namespace_map(crate_to_namespace_map.clone()); + Ok(ci) + }) + .collect::>>()?; + + // give every CI a cloned copy of every CI - including itself for simplicity. + // we end up taking n^2 copies of all ci's, but it works. + let ci_list2 = ci_list.clone(); + ci_list + .iter_mut() + .for_each(|ci| ci.set_all_component_interfaces(ci_list2.clone())); + Ok(ci_list) + } + + fn load_udl_metadata( + &self, + group: &MetadataGroup, + crate_name: &str, + ) -> Result> { + let udl_items = group + .items + .iter() + .filter_map(|i| match i { + Metadata::UdlFile(meta) => Some(meta), + _ => None, + }) + .collect::>(); + // We only support 1 UDL file per crate, for no good reason! + match udl_items.len() { + 0 => Ok(None), + 1 => { + if udl_items[0].module_path != crate_name { + bail!( + "UDL is for crate '{}' but this crate name is '{}'", + udl_items[0].module_path, + crate_name + ); + } + let udl = self + .config_supplier + .get_udl(crate_name, &udl_items[0].file_stub)?; + let udl_group = uniffi_udl::parse_udl(&udl, crate_name)?; + Ok(Some(udl_group)) + } + n => bail!("{n} UDL files found for {crate_name}"), + } + } + + /// Load a [Component] list + /// + /// This groups [ComponentInterface] values with configuration data from `uniffi.toml` files. + /// Pass in a `parse_config` function which parses raw TOML into your language-specific config structure. + /// + /// Note: the TOML data will contain the entire config tree. + /// You probably want to do something like + /// `toml.get("bindings").and_then(|b| b.get("my-language-key"))` + /// to extract the data for your bindings. + pub fn load_components( + &self, + cis: Vec, + mut parse_config: P, + ) -> Result>> + where + P: FnMut(&ComponentInterface, toml::Value) -> Result, + Config: Default, + { + cis.into_iter() + .map(|ci| { + let toml = self + .config_supplier + .get_toml(ci.crate_name())? + .unwrap_or_default(); + let config = parse_config(&ci, toml.into())?; + Ok(Component { ci, config }) + }) + .collect() + } + + /// Get the basename for a source file + /// + /// This will remove any file extension. + /// For libraries it will remove the leading `lib`. + pub fn source_basename<'a>(&self, source_path: &'a Utf8Path) -> &'a str { + let mut basename = match source_path.file_stem() { + Some(stem) => stem, + None => source_path.as_str(), + }; + if !self.is_udl(source_path) { + basename = match basename.strip_prefix("lib") { + Some(name) => name, + None => basename, + } + }; + basename + } + + fn is_udl(&self, source_path: &Utf8Path) -> bool { + matches!( + source_path.extension(), + Some(ext) if ext.to_lowercase() == "udl" + ) + } +} diff --git a/uniffi_bindgen/src/macro_metadata/mod.rs b/uniffi_bindgen/src/macro_metadata/mod.rs index bc5a0a790f..1bbf13ff09 100644 --- a/uniffi_bindgen/src/macro_metadata/mod.rs +++ b/uniffi_bindgen/src/macro_metadata/mod.rs @@ -10,7 +10,7 @@ mod ci; mod extract; pub use ci::{add_group_to_ci, add_to_ci}; -pub use extract::extract_from_library; +pub use extract::{extract_from_bytes, extract_from_library}; pub fn add_to_ci_from_library( iface: &mut ComponentInterface, diff --git a/uniffi_meta/src/group.rs b/uniffi_meta/src/group.rs index 7842009c14..c0a2d1e841 100644 --- a/uniffi_meta/src/group.rs +++ b/uniffi_meta/src/group.rs @@ -7,7 +7,7 @@ use std::collections::{BTreeSet, HashMap}; use crate::*; use anyhow::{bail, Result}; -type MetadataGroupMap = HashMap; +pub type MetadataGroupMap = HashMap; // Create empty metadata groups based on the metadata items. pub fn create_metadata_groups(items: &[Metadata]) -> MetadataGroupMap { diff --git a/uniffi_meta/src/lib.rs b/uniffi_meta/src/lib.rs index 239de0fe68..4a34a27cb3 100644 --- a/uniffi_meta/src/lib.rs +++ b/uniffi_meta/src/lib.rs @@ -9,7 +9,7 @@ mod ffi_names; pub use ffi_names::*; mod group; -pub use group::{create_metadata_groups, group_metadata, MetadataGroup}; +pub use group::{create_metadata_groups, group_metadata, MetadataGroup, MetadataGroupMap}; mod reader; pub use reader::{read_metadata, read_metadata_type};