diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7e2afe0b..bc04184b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ ## [[UnreleasedUniFFIVersion]] (backend crates: [[UnreleasedBackendVersion]]) - (_[[ReleaseDate]]_) +### What's new? + +- Added the `uniffi-bindgen-swift` binary. It works like `uniffi-bindgen` but with additional + Swift-specific features. See + https://mozilla.github.io/uniffi-rs/latest/swift/uniffi-bindgen-swift.html for details. + ### What's fixed? - `uniffi.toml` of crates without a `lib` type where ignored in 0.28.1 diff --git a/docs/manual/src/swift/configuration.md b/docs/manual/src/swift/configuration.md index cc2fd92d41..3965c5a7fa 100644 --- a/docs/manual/src/swift/configuration.md +++ b/docs/manual/src/swift/configuration.md @@ -13,7 +13,7 @@ more likely to change than other configurations. | `module_name` | `{namespace}`[^1] | The name of the Swift module containing the high-level foreign-language bindings. | | `ffi_module_name` | `{module_name}FFI` | The name of the lower-level C module containing the FFI declarations. | | `ffi_module_filename` | `{ffi_module_name}` | The filename stem for the lower-level C module containing the FFI declarations. | -| `generate_module_map` | `true` | Whether to generate a `.modulemap` file for the lower-level C module with FFI declarations. | +| `generate_module_map` | `true` | Whether to generate a `.modulemap` file for the lower-level C module with FFI declarations. (ignored by `uniffi-bindgen-swift`) | | `omit_argument_labels` | `false` | Whether to omit argument labels in Swift function definitions. | | `generate_immutable_records` | `false` | Whether to generate records with immutable fields (`let` instead of `var`). | | `experimental_sendable_value_types` | `false` | Whether to mark value types as `Sendable'. | diff --git a/docs/manual/src/swift/overview.md b/docs/manual/src/swift/overview.md index 374cd0e1c5..af19c401a1 100644 --- a/docs/manual/src/swift/overview.md +++ b/docs/manual/src/swift/overview.md @@ -25,20 +25,10 @@ Concepts from the UDL file map into Swift as follows: * If this happens inside a non-throwing Swift function, it will be converted into a fatal Swift error that cannot be caught. -Conceptually, the generated bindings are split into two Swift modules, one for the low-level -C FFI layer and one for the higher-level Swift bindings. For a UniFFI component named "example" -we generate: +## Generated files -* A C header file `exampleFFI.h` declaring the low-level structs and functions for calling - into Rust, along with a corresponding `exampleFFI.modulemap` to expose them to Swift. -* A Swift source file `example.swift` that imports the `exampleFFI` module and wraps it - to provide the higher-level Swift API. +UniFFI generates several kinds of files for Swift bindings: -Splitting up the bindings in this way gives you flexibility over how both the Rust code -and the Swift code are distributed to consumers. For example, you may choose to compile -and distribute the Rust code for several UniFFI components as a single shared library -in order to reduce the compiled code size, while distributing their Swift wrappers as -individual modules. - -For more technical details on how the bindings work internally, please see the -[module documentation](https://docs.rs/uniffi_bindgen/latest/uniffi_bindgen/bindings/swift/index.html) +* C header files declaring the FFI structs/functions used by the Rust scaffolding code +* A modulemap, which defines a Swift module for the C FFI definitions in the header file. +* A Swift source file that defines the Swift API used by consumers. This imports the FFI module. diff --git a/docs/manual/src/swift/uniffi-bindgen-swift.md b/docs/manual/src/swift/uniffi-bindgen-swift.md new file mode 100644 index 0000000000..ab293657b3 --- /dev/null +++ b/docs/manual/src/swift/uniffi-bindgen-swift.md @@ -0,0 +1,48 @@ +# uniffi-bindgen-swift + +Swift bindings can be generated like other languages using `uniffi-bindgen -l swift`. However, you +can also use the `uniffi-bindgen-swift` binary which gives greater control over Swift-specific +features: + +* Select which kind of files to generate: headers, modulemaps, and/or Swift sources. +* Generate a single modulemap for a library. +* Generate XCFramework-compatible modulemaps. +* Customize the modulemap module name. +* Customize the modulemap filename. + +`uniffi-bindgen-swift` can be added to your project using the same general steps as `uniffi-bindgen`. +See https://mozilla.github.io/uniffi-rs/latest/tutorial/foreign_language_bindings.html#creating-the-bindgen-binary. +The Rust source for the binary should be: + +``` +fn main() { + uniffi::uniffi_bindgen_swift() +} +``` + +`uniffi-bindgen-swift` always inputs a library path and runs in "library mode". This means +proc-macro-based bindings generation is always supported. + +## Examples: + + +Generate .swift source files for a library +``` +cargo run -p uniffi-bindgen-swift -- target/release/mylibrary.a build/swift --swift-sources +``` + +Generate .h files for a library +``` +cargo run -p uniffi-bindgen-swift -- target/release/mylibrary.a build/swift/Headers --headers +``` + + +Generate a modulemap +``` +cargo run -p uniffi-bindgen-swift -- target/release/mylibrary.a build/swift/Modules --modulemap --modulemap-filename mymodule.modulemap +``` + +Generate a Xcframework-compatible modulemap +``` +cargo run -p uniffi-bindgen-swift -- target/release/mylibrary.a build/swift/Modules --xcframework --modulemap --modulemap-filename mymodule.modulemap +``` diff --git a/mkdocs.yml b/mkdocs.yml index 9a9de54a9c..789adf479c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,6 +69,7 @@ nav: - 'Swift': - ./swift/overview.md + - ./swift/uniffi-bindgen-swift.md - ./swift/configuration.md - ./swift/module.md - ./swift/xcode.md diff --git a/uniffi/src/cli/mod.rs b/uniffi/src/cli/mod.rs new file mode 100644 index 0000000000..ab4bb0d356 --- /dev/null +++ b/uniffi/src/cli/mod.rs @@ -0,0 +1,20 @@ +/* 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/. */ + +mod swift; +mod uniffi_bindgen; + +pub fn uniffi_bindgen_main() { + if let Err(e) = uniffi_bindgen::run_main() { + eprintln!("{e}"); + std::process::exit(1); + } +} + +pub fn uniffi_bindgen_swift() { + if let Err(e) = swift::run_main() { + eprintln!("{e}"); + std::process::exit(1); + } +} diff --git a/uniffi/src/cli/swift.rs b/uniffi/src/cli/swift.rs new file mode 100644 index 0000000000..a74cd9a7ef --- /dev/null +++ b/uniffi/src/cli/swift.rs @@ -0,0 +1,73 @@ +/* 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 anyhow::Result; +use camino::Utf8PathBuf; +use clap::{Args, Parser}; + +use uniffi_bindgen::bindings::{generate_swift_bindings, SwiftBindingsOptions}; + +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +struct Cli { + #[command(flatten)] + kinds: Kinds, + /// Library path to generate bindings for + library_path: Utf8PathBuf, + /// Directory to generate files in + out_dir: Utf8PathBuf, + /// Generate a XCFramework-compatible modulemap + #[arg(long)] + xcframework: bool, + /// module name for the generated modulemap + #[arg(long)] + module_name: Option, + /// filename for the generate modulemap + #[arg(long)] + modulemap_filename: Option, + /// Whether we should exclude dependencies when running "cargo metadata". + /// This will mean external types may not be resolved if they are implemented in crates + /// outside of this workspace. + /// This can be used in environments when all types are in the namespace and fetching + /// all sub-dependencies causes obscure platform specific problems. + #[clap(long)] + metadata_no_deps: bool, +} + +#[derive(Debug, Args)] +#[group(required = true, multiple = true)] +struct Kinds { + /// Generate swift files + #[arg(long)] + swift_sources: bool, + + /// Generate header files + #[arg(long)] + headers: bool, + + /// Generate modulemap + #[arg(long)] + modulemap: bool, +} + +pub fn run_main() -> Result<()> { + let cli = Cli::parse(); + generate_swift_bindings(cli.into()) +} + +impl From for SwiftBindingsOptions { + fn from(cli: Cli) -> Self { + Self { + generate_swift_sources: cli.kinds.swift_sources, + generate_headers: cli.kinds.headers, + generate_modulemap: cli.kinds.modulemap, + library_path: cli.library_path, + out_dir: cli.out_dir, + xcframework: cli.xcframework, + module_name: cli.module_name, + modulemap_filename: cli.modulemap_filename, + metadata_no_deps: cli.metadata_no_deps, + } + } +} diff --git a/uniffi/src/cli.rs b/uniffi/src/cli/uniffi_bindgen.rs similarity index 100% rename from uniffi/src/cli.rs rename to uniffi/src/cli/uniffi_bindgen.rs diff --git a/uniffi/src/lib.rs b/uniffi/src/lib.rs index 43c933c38c..42b94c0438 100644 --- a/uniffi/src/lib.rs +++ b/uniffi/src/lib.rs @@ -28,9 +28,7 @@ pub use uniffi_build::{generate_scaffolding, generate_scaffolding_for_crate}; pub use uniffi_macros::build_foreign_language_testcases; #[cfg(feature = "cli")] -pub fn uniffi_bindgen_main() { - cli::run_main().unwrap(); -} +pub use cli::*; #[cfg(test)] mod test { diff --git a/uniffi_bindgen/src/bindings/mod.rs b/uniffi_bindgen/src/bindings/mod.rs index 2d6cdc07fb..f76dea50b6 100644 --- a/uniffi_bindgen/src/bindings/mod.rs +++ b/uniffi_bindgen/src/bindings/mod.rs @@ -14,7 +14,7 @@ pub use python::PythonBindingGenerator; mod ruby; pub use ruby::RubyBindingGenerator; mod swift; -pub use swift::SwiftBindingGenerator; +pub use swift::{generate_swift_bindings, SwiftBindingGenerator, SwiftBindingsOptions}; #[cfg(feature = "bindgen-tests")] pub use self::{ diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index c81495a8c6..4d1440262b 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -267,7 +267,6 @@ impl Config { } /// Generate UniFFI component bindings for Swift, as strings in memory. -/// pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result { let header = BridgingHeader::new(config, ci) .render() @@ -277,7 +276,7 @@ pub fn generate_bindings(config: &Config, ci: &ComponentInterface) -> Result Result Result { + BridgingHeader::new(config, ci) + .render() + .context("failed to render Swift bridging header") +} + +/// Generate the swift source for a component +pub fn generate_swift(config: &Config, ci: &ComponentInterface) -> Result { + SwiftWrapper::new(config.clone(), ci) + .render() + .context("failed to render Swift library") +} + +/// Generate the modulemap for a set of components +pub fn generate_modulemap( + module_name: String, + header_filenames: Vec, + xcframework: bool, +) -> Result { + ModuleMap { + module_name, + header_filenames, + xcframework, + } + .render() + .context("failed to render Swift library") +} + /// Renders Swift helper code for all types /// /// This template is a bit different than others in that it stores internal state from the render @@ -369,14 +397,19 @@ impl<'config, 'ci> BridgingHeader<'config, 'ci> { /// so that it can be imported by the higher-level code in from [`SwiftWrapper`]. #[derive(Template)] #[template(syntax = "c", escape = "none", path = "ModuleMapTemplate.modulemap")] -pub struct ModuleMap<'config, 'ci> { - config: &'config Config, - _ci: &'ci ComponentInterface, +pub struct ModuleMap { + module_name: String, + header_filenames: Vec, + xcframework: bool, } -impl<'config, 'ci> ModuleMap<'config, 'ci> { - pub fn new(config: &'config Config, _ci: &'ci ComponentInterface) -> Self { - Self { config, _ci } +impl ModuleMap { + pub fn new_for_single_component(config: &Config, _ci: &ComponentInterface) -> Self { + Self { + module_name: config.ffi_module_name(), + header_filenames: vec![config.header_filename()], + xcframework: false, + } } } diff --git a/uniffi_bindgen/src/bindings/swift/mod.rs b/uniffi_bindgen/src/bindings/swift/mod.rs index 27d2bcf0a3..9f69ea68d9 100644 --- a/uniffi_bindgen/src/bindings/swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/mod.rs @@ -30,12 +30,13 @@ //! use crate::{BindingGenerator, Component, GenerationSettings}; -use anyhow::Result; +use anyhow::{Context, Result}; +use camino::Utf8PathBuf; use fs_err as fs; use std::process::Command; mod gen_swift; -use gen_swift::{generate_bindings, Config}; +use gen_swift::{generate_bindings, generate_header, generate_modulemap, generate_swift, Config}; #[cfg(feature = "bindgen-tests")] pub mod test; @@ -121,3 +122,98 @@ impl BindingGenerator for SwiftBindingGenerator { Ok(()) } } + +/// Generate Swift bindings +/// +/// This is used by the uniffi-bindgen-swift command, which supports Swift-specific options. +/// +/// In the future, we may want to replace the generalized `uniffi-bindgen` with a set of +/// specialized `uniffi-bindgen-[language]` commands. +pub fn generate_swift_bindings(options: SwiftBindingsOptions) -> Result<()> { + #[cfg(feature = "cargo-metadata")] + let config_supplier = { + use crate::cargo_metadata::CrateConfigSupplier; + let mut cmd = cargo_metadata::MetadataCommand::new(); + if options.metadata_no_deps { + cmd.no_deps(); + } + let metadata = cmd.exec().context("error running cargo metadata")?; + CrateConfigSupplier::from(metadata) + }; + #[cfg(not(feature = "cargo-metadata"))] + let config_supplier = crate::EmptyCrateConfigSupplier; + + 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)?; + + for Component { ci, config } in &components { + if options.generate_swift_sources { + let source_file = options + .out_dir + .join(format!("{}.swift", config.module_name())); + fs::write(&source_file, generate_swift(config, ci)?)?; + } + + if options.generate_headers { + let header_file = options.out_dir.join(config.header_filename()); + fs::write(header_file, generate_header(config, ci)?)?; + } + } + + // 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, + } + }; + + let module_name = options + .module_name + .unwrap_or_else(|| library_name.to_string()); + let modulemap_filename = options + .modulemap_filename + .unwrap_or_else(|| format!("{library_name}.modulemap")); + + if options.generate_modulemap { + let mut header_filenames: Vec<_> = components + .iter() + .map(|Component { config, .. }| config.header_filename()) + .collect(); + header_filenames.sort(); + let modulemap_source = + generate_modulemap(module_name, header_filenames, options.xcframework)?; + let modulemap_path = options.out_dir.join(modulemap_filename); + fs::write(modulemap_path, modulemap_source)?; + } + + Ok(()) +} + +#[derive(Debug)] +pub struct SwiftBindingsOptions { + pub generate_swift_sources: bool, + pub generate_headers: bool, + pub generate_modulemap: bool, + pub library_path: Utf8PathBuf, + pub out_dir: Utf8PathBuf, + pub xcframework: bool, + pub module_name: Option, + pub modulemap_filename: Option, + pub metadata_no_deps: bool, +} diff --git a/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap b/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap index f5f73ceb1b..3b39f42f82 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap +++ b/uniffi_bindgen/src/bindings/swift/templates/ModuleMapTemplate.modulemap @@ -1,6 +1,6 @@ -// This file was autogenerated by some hot garbage in the `uniffi` crate. -// Trust me, you don't want to mess with it! -module {{ config.ffi_module_name() }} { - header "{{ config.header_filename() }}" +{% if xcframework %}framework {%endif %}module {{ module_name }} { + {%- for filename in header_filenames %} + header "{{ filename }}" + {%- endfor %} export * }