Skip to content

fix(uniffi): link-time optimisation reaches the libraries the release ships - #579

Merged
bahdotsh merged 2 commits into
mainfrom
fix/uniffi-lto
Oct 19, 2026
Merged

bahdotsh merged 2 commits into
mainfrom
fix/uniffi-lto

Conversation

@bahdotsh

Copy link
Copy Markdown
Member

What was wrong

The release profile sets lto = true, but cargo runs link-time optimisation on a library only when every crate type in its list can take it. The FFI crate listed ["cdylib", "staticlib", "lib"], and an rlib cannot, so cargo passed no LTO flag at all. Every native library from the UniFFI migration (2025-11-07) to 0.29.0 was built without it, on every platform, and no build said so.

The rule is in cargo's source (CrateType::can_lto and the comment in compiler/lto.rs: "a cdylib/rlib combination won't allow LTO") and is cargo issue rust-lang/cargo#2301, closed in 2017 with a workaround and no warning.

The change

  • The crate's types are now cdylib and staticlib. The release build now passes -C lto.
  • tools/capi-fuzz was the only thing linking the crate as Rust. It now compiles the crate's own lib.rs under its own package, with its own build script generating the scaffolding from the same interface definition. The fuzz target is unchanged.
  • Two guards: the_ffi_crate_builds_no_rust_library refuses a crate type that turns LTO off; the_fuzzer_compiles_the_ffi_crate_with_its_dependencies holds the fuzzer's dependencies and features to the crate's, since the fuzz build only runs nightly.

Measured

macOS arm64, same commit, fresh target directory each, capi on:

before after
liboffline_protocol_uniffi.a 137,949,536 23,393,288
liboffline_protocol_uniffi.dylib 11,534,416 10,932,208
exported C and UniFFI functions 482 482
build time / peak memory 1m46 / 1.3 GB 2m45 / 2.1 GB

The static library is what the iOS framework and the npm package carry, so that is where nearly all of the win lands. The iOS and Android numbers will be read off the first release artifacts built this way.

Verified locally

  • cargo fmt, cargo clippy --workspace -- -D warnings, clippy without the C binding, cargo test --workspace --lib, rustdoc under -D warnings.
  • The uniffi crate's 263 unit tests all still run: it never had a doctest, so dropping the rlib loses none.
  • The C consumer check (shared, static, exchange, quick start) and scripts/check-c-library.sh macos-arm64 against the LTO release dylib.
  • The Python suite against the LTO release dylib: 1124 passed, 4 skipped.
  • Both guards negative-controlled six ways.
  • The fuzzer: every seed runs, and cargo +nightly fuzz run --sanitizer none for 60 s on macOS ran 1.4 million inputs with coverage rising to 2015 edges. Fuzzing on macOS did not link before this change; fuzz.yml and docs/bridges/c.md now say so.

Not covered here

  • fuzz.yml has never run. I will dispatch it on this branch.
  • The mobile profile (opt-level and panic strategy) is a separate PR: no build has ever used minisize, while six documents say the mobile artifact ships panic = "abort".
  • A Rust consumer of the crate from crates.io (0.29.0 has none) can no longer depend on it.

…ibrary

tools/capi-fuzz depended on offline-protocol-uniffi by path, which needs the
crate to build a Rust library. It now compiles the crate's own lib.rs under
its own package, generates the scaffolding from the crate's interface
definition in its own build script, and declares the crate's dependencies
and features itself. The fuzz target is unchanged.

Because the fuzz build no longer links the crate's cdylib, whose coverage
hooks only the fuzz binary defined, the same command now runs on macOS with
`--sanitizer none`. Checked there: every seed runs, and a 60 second session
ran 1.4 million inputs with coverage growing to 2015 edges and no crash.
… ships

Cargo runs LTO on a library only when every crate type in its list can take
it. The FFI crate listed `lib` beside `cdylib` and `staticlib`, and an rlib
cannot, so `lto = true` in the release profile never reached a shipped
library from the UniFFI migration to 0.29.0, with no warning. Nothing links
the crate as Rust: the one consumer, the fuzzer, now compiles its source.

Measured on macOS arm64 at the same commit, fresh target directories: the
static library falls from 137,949,536 to 23,393,288 bytes and the shared
library from 11,534,416 to 10,932,208, with the same 482 exported functions.
The release build passes `-C lto`; the C consumer check, the release C
library check and the Python suite (1124 passed) run against the result.

Two guards: `the_ffi_crate_builds_no_rust_library` refuses a crate type
that turns LTO off, and `the_fuzzer_compiles_the_ffi_crate_with_its_dependencies`
holds the fuzzer's copy of the dependencies and features to the crate's.
Each was negative-controlled (rlib, lib, a dropped dependency, a dropped
feature of a dependency, a new crate feature, a moved interface definition).
@bahdotsh
bahdotsh merged commit 6f3920a into main Oct 19, 2026
28 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant