Skip to content

Commit

Permalink
Merge pull request #2235 from bendk/push-szzsnwnoozpz
Browse files Browse the repository at this point in the history
Generate a single modulemap in library mode
  • Loading branch information
bendk committed Sep 16, 2024
2 parents 3514060 + fe71008 commit 8aec9df
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 43 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## [[UnreleasedUniFFIVersion]] (backend crates: [[UnreleasedBackendVersion]]) - (_[[ReleaseDate]]_)

### ⚠️ Breaking Changes for external bindings authors ⚠️

- Added the `GenerationSettings::mode` field. This can be ignored in most cases, it's currently only used by Swift.

[All changes in [[UnreleasedUniFFIVersion]]](https://github.com/mozilla/uniffi-rs/compare/v0.28.1...HEAD).

## v0.28.1 (backend crates: v0.28.1) - (_2024-08-09_)
Expand Down
4 changes: 2 additions & 2 deletions docs/manual/src/swift/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ more likely to change than other configurations.

| Configuration name | Default | Description |
| ----------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cdylib_name` | `uniffi_{namespace}`[^1] | The name of the compiled Rust library containing the FFI implementation (not needed when using `generate --library`). |
| `cdylib_name` | `uniffi_{namespace}`[^1] | The name of the compiled Rust library containing the FFI implementation (not needed when using `--library`). |
| `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. Not valid when using `--library`, in library-mode the modulemap is always generated.
| `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'. |
Expand Down
43 changes: 29 additions & 14 deletions docs/manual/src/swift/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,35 @@ 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:

* 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.

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.
## Generated files

UniFFI generates several kinds of files for Swift bindings:

* 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.

The file layout depends on which mode is used to generate the bindings:

### Library mode

`uniffi-bindgen` in [library mode](../tutorial/foreign_language_bindings.html#running-uniffi-bindgen-using-a-library-file) generates:

* A Swift file for each crate (`[crate_name].swift`)
* A header file for each crate (`[crate_name]FFI.h`)
* A single modulemap file for the entire module (`[library_name].modulemap`)

The expectation is that each `.swift` file will be compiled together into a single Swift module that represents the library as a whole.

### Single UDL file

`uniffi-bindgen` in [Single UDL file mode](../tutorial/foreign_language_bindings.html#running-uniffi-bindgen-with-a-single-udl-file) generates:

* A Swift file for the crate (`[crate_name].swift`)
* A header file for the crate (`[crate_name]FFI.h`)
* A modulemap file for the crate (`[crate_name].modulemap`)

The expectation is that the `.swift` will be compiled into a module and this is the only module to generate for the Rust library.

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)
56 changes: 51 additions & 5 deletions uniffi_bindgen/src/bindings/swift/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
//! * How to read from and write into a byte buffer.
//!

use crate::{BindingGenerator, Component, GenerationSettings};
use anyhow::Result;
use crate::{BindingGenerator, Component, GenerationMode, GenerationSettings};
use anyhow::{bail, Result};
use fs_err as fs;
use std::process::Command;

Expand Down Expand Up @@ -66,10 +66,15 @@ impl BindingGenerator for SwiftBindingGenerator {

fn update_component_configs(
&self,
_settings: &GenerationSettings,
settings: &GenerationSettings,
components: &mut Vec<Component<Self::Config>>,
) -> Result<()> {
for c in &mut *components {
if matches!(settings.mode, GenerationMode::Library { .. })
&& !c.config.generate_module_map()
{
bail!("generate_module_map=false is invalid for library mode, module maps will always be generated");
}
c.config
.module_name
.get_or_insert_with(|| c.ci.namespace().into());
Expand All @@ -85,6 +90,13 @@ impl BindingGenerator for SwiftBindingGenerator {
settings: &GenerationSettings,
components: &[Component<Self::Config>],
) -> Result<()> {
if components.is_empty() {
bail!("write_bindings called with 0 components");
}

// Collects the module maps generated for all components
let mut module_maps = vec![];

for Component { ci, config, .. } in components {
let Bindings {
header,
Expand All @@ -101,8 +113,7 @@ impl BindingGenerator for SwiftBindingGenerator {
fs::write(header_file, header)?;

if let Some(modulemap) = modulemap {
let modulemap_file = settings.out_dir.join(config.modulemap_filename());
fs::write(modulemap_file, modulemap)?;
module_maps.push((config.modulemap_filename(), modulemap));
}

if settings.try_format_code {
Expand All @@ -118,6 +129,41 @@ impl BindingGenerator for SwiftBindingGenerator {
}
}

match &settings.mode {
GenerationMode::SingleComponent => {
match module_maps.as_slice() {
// No module maps since generate_module_map was false
[] => (),
// Normal case: 1 module map
[(modulemap_filename, modulemap)] => {
let modulemap_file = settings.out_dir.join(modulemap_filename);
fs::write(modulemap_file, modulemap)?;
}
// Something weird happened if we have multiple module maps
module_maps => {
bail!("UniFFI internal error: {} module maps generated in GenerationMode::SingleComponent", module_maps.len());
}
}
}
// In library mode, we expect there to be multiple module maps that we will combine
// into one.
GenerationMode::Library { library_path } => {
let library_filename = match library_path.file_name() {
Some(f) => f,
None => bail!("{library_path} has no filename"),
};
let modulemap_path = settings
.out_dir
.join(format!("{library_filename}.modulemap"));
let modulemap_sources = module_maps
.into_iter()
.map(|(_, source)| source)
.collect::<Vec<_>>();

fs::write(modulemap_path, modulemap_sources.join("\n"))?;
}
}

Ok(())
}
}
22 changes: 2 additions & 20 deletions uniffi_bindgen/src/bindings/swift/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::Metadata;
use std::env::consts::{DLL_PREFIX, DLL_SUFFIX};
use std::ffi::OsStr;
use std::fs::{read_to_string, File};
use std::io::Write;
use std::process::{Command, Stdio};
use uniffi_testing::UniFFITestHelper;

Expand Down Expand Up @@ -153,25 +151,9 @@ impl GeneratedSources {
let main_module = main_source.config.module_name();
let modulemap_glob = glob(&out_dir.join("*.modulemap"))?;
let module_map = match modulemap_glob.len() {
0 => bail!("No modulemap files found in {out_dir}"),
// Normally we only generate 1 module map and can return it directly
// write_bindings should have generated exactly 1 module map
1 => modulemap_glob.into_iter().next().unwrap(),
// When we use multiple UDL files in a test, for example the ext-types fixture,
// then we get multiple module maps and need to combine them
_ => {
let path = out_dir.join("combined.modulemap");
let mut f = File::create(&path)?;
write!(
f,
"{}",
modulemap_glob
.into_iter()
.map(|path| Ok(read_to_string(path)?))
.collect::<Result<Vec<String>>>()?
.join("\n")
)?;
path
}
n => bail!("{n} modulemap files found in {out_dir}"),
};

Ok(GeneratedSources {
Expand Down
18 changes: 17 additions & 1 deletion uniffi_bindgen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,26 @@ use uniffi_meta::Type;
/// the generator itself.
// TODO: We should try and move the public interface of the module to
// this struct. For now, only the BindingGenerator uses it.
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct GenerationSettings {
pub out_dir: Utf8PathBuf,
pub try_format_code: bool,
pub cdylib: Option<String>,
pub mode: GenerationMode,
}

#[derive(Debug)]
pub enum GenerationMode {
/// Generating code for a single component
SingleComponent,
/// Generating code for all components in a library (i.e. --library was present on the CLI).
/// This affects generation for some languages, for example Swift will generate a single
/// modulemap instead of one module map per-component.
///
/// Note: it's possible that this is still only generating code for a single component, but we
/// should still use "library mode behavior", so that the user can seamlessly add more
/// components to the library.
Library { library_path: Utf8PathBuf },
}

/// A trait representing a UniFFI Binding Generator
Expand Down Expand Up @@ -315,6 +330,7 @@ pub fn generate_external_bindings<T: BindingGenerator>(
out_dir_override.as_ref().map(|p| p.as_ref()),
)?,
try_format_code,
mode: GenerationMode::SingleComponent,
};

let mut components = vec![Component { ci, config }];
Expand Down
5 changes: 4 additions & 1 deletion uniffi_bindgen/src/library_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/// package maps.
use crate::{
macro_metadata, overridden_config_value, BindgenCrateConfigSupplier, BindingGenerator,
Component, ComponentInterface, GenerationSettings, Result,
Component, ComponentInterface, GenerationMode, GenerationSettings, Result,
};
use anyhow::bail;
use camino::Utf8Path;
Expand Down Expand Up @@ -56,6 +56,9 @@ pub fn generate_bindings<T: BindingGenerator + ?Sized>(
out_dir: out_dir.to_owned(),
try_format_code,
cdylib: calc_cdylib_name(library_path).map(ToOwned::to_owned),
mode: GenerationMode::Library {
library_path: library_path.to_owned(),
},
};
binding_generator.update_component_configs(&settings, &mut components)?;

Expand Down

0 comments on commit 8aec9df

Please sign in to comment.