Skip to content

feat(uniffi): same-named types across crates in every generator - #21

Merged
kixelated merged 25 commits into
mainfrom
quest/m2/uniffi-namespaces
Oct 24, 2026
Merged

kixelated merged 25 commits into
mainfrom
quest/m2/uniffi-namespaces

Conversation

@kixelated

@kixelated kixelated commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Problem

Two UniFFI crates linked into one library could not each export a type with the same name, so media.GroupConsumer(group: GroupConsumer) was impossible: every generator built on ComponentInterface refused it with conflicting types, and Python silently dropped the local type (mozilla/uniffi-rs#3026, unfixed upstream). This finishes the namespaces quest; moq's per-layer crate split (moq-dev/moq#5460) is the consumer.

Approach

  • Core (uniffi_bindgen): TypeUniverse, the set of errors, and the recursion graph key named types by crate and name. is_name_used_as_error(name) becomes is_used_as_error(&ty), get_type(name) is local-only, and the recursive type walk resolves an external type in its own crate's interface instead of falling back to a same-named local one (also when the type lives in a submodule).
  • Python: cherry-picks mozilla/uniffi-rs#2985 (the sort no longer drops a same-named definition), keys the pipeline's errors and recursion by namespace, and names container helpers for an external type after its namespace.
  • Every generator references another crate's type through that crate's module, and names the helpers it generates for containers of an external type after its namespace (SequenceNetTypeFrame next to SequenceTypeFrame):
    • Kotlin: fully qualified packages (no imports that a local type can shadow), carried by a KotlinCi { ci, config } context.
    • Swift: qualifies an external type with its module only when its name is ambiguous, and imports that module. Same-named types need one Swift module per crate; everything else keeps the single-module layout and output. The test harness compiles one module per crate when the bindings import each other.
    • Go: already package-qualified; error checks are type-aware.
    • Dart: imports another crate's library as uniffi_<namespace> (a bare net prefix is hidden by a member named net). The shared runtime now comes from the same crate every run, and the harness drives library mode.
    • C++: global-namespace qualification (::net::Frame), aliased converters, per-crate definition sort and strip_type_prefix.
    • C: supports external types (it refused them before); every C name carries its namespace, and a consumer compiles each crate's C output.
  • Fixture uniffi/fixtures/namespaces/{net,media}: both export GroupConsumer, Frame, Priority, and GroupError, each media type holds or wraps its net namesake, and containers hold both. Kotlin, Swift, Python, Go, Dart, C++, and C each compile and run a consumer.

No FFI contract change: metadata, checksums, and scaffolding are untouched, so the contract quest stays as it is.

Proof: moq-ffi bindings are byte-identical

moq-ffi at moq-dev/moq@4a16316 (default features, crates.io uniffi 0.32), generated for Kotlin, Swift, and Python (--no-format), Go, Dart (dart/uniffi.toml, --no-format), and C++ (cpp/moq/uniffi.toml):

Generators Result
moq's pins (kixelated/uniffi-rs@88d835d, Go v0.10.0-kixelated.1, moq's flake Dart and C++) baseline
this branch all 17 files byte-identical

The C backend refuses moq-ffi with both, with the same error (duplicate moq_audio_encoder_finish). Each generator's fork also diffed its own single-crate fixtures before and after: identical. Only crates with external types change.

Checks

  • just check and just test pass.
  • just uniffi fixtures in nix develop .#uniffi: 355 nextest tests (Kotlin, Swift, Python, Dart fixtures, including the new namespaces consumers), 60 bindgen tests, Go go test, and 33 C++/C ctest cases pass. The new C++ and C consumers are clean under valgrind memcheck (run by hand, since valgrind is not in the dev shell).
  • One local caveat: the Kotlin harness also loads the cdylib from target/debug, so a stale dev build of a fixture fails with UnsatisfiedLinkError until rebuilt. CI builds from clean.

Impact

  • uniffi_bindgen (breaking, in uniffi/MIGRATE.md): ComponentInterface::is_name_used_as_error is replaced by is_used_as_error(&impl AsType); get_type finds only local types; find_component_interface accepts a module path; VisitMut::visit_error_name takes the defining module path.
  • Generated code for crates that use another crate's types: external references are qualified (Kotlin, Swift when ambiguous, Dart, C++), and private container helper names change. Single-crate output is unchanged.
  • Generated C: external types are supported; every name carries the namespace prefix (breaking for a crate whose items lacked it; in uniffi/MIGRATE.md).
  • New fixtures: uniffi-fixture-namespaces-{net,media} and dart_namespaces (unpublished).

Alternatives

  • Qualify only colliding names everywhere: smaller diffs for existing multi-crate users, but every generator would need collision detection; always qualifying is simpler where the language allows it. Swift is the exception, because qualification needs per-crate modules and the documented layout is one module.
  • Thread-local context for Kotlin's packages and Swift's modules: smaller diff, but hidden state; both pass context explicitly.
  • Wait for upstream: the issue is unfixed on uniffi main.

Follow-ups

  • moq: re-run the per-layer crate split decision (rejected in quest(m1): binding namespace spike is a no-go moq-dev/moq#5460) on this fork.
  • Generated qualifiers can still be hidden by a generated member with the same name as a package root (for example a Kotlin field named uniffi); Dart avoids it with its uniffi_ prefix, C++ with ::.
  • uniffi-bindgen-swift's xcframework path compiles all crates into one module, so same-named types still cannot use it.
  • In Go and Dart, the container helpers for an external net::Frame and for a local type named NetFrame share a name. The generated code then fails to compile instead of mixing them up.

(written by Opus 5.5)

🤖 Generated with Claude Code

https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn

kixelated and others added 23 commits October 24, 2026 00:32
…drop

TypeUniverse, the error set, and the recursion graph key named types by
crate and name, so a crate can use another crate's type of the same name.
Python's pipeline keeps both definitions (mozilla/uniffi-rs#2985) and
names container helpers for external types after their namespace.

Adds the two-crate namespaces fixture with its Python consumer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
External types' compound helpers carry their namespace, and error-ness
comes from the type rather than its bare name. Adds the namespaces
fixture to the Go fixtures with its consumer test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
Go cannot hold a function and a record both named Bounds in one package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
Go cannot hold a function and a record both named Bounds in one package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
Go cannot hold a function and a record both named Bounds in one package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
Go cannot hold a function and a record both named Bounds in one package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
Go cannot hold a function and a record both named Bounds in one package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
C++: canonical and converter names take the interface, so an external
type's helpers carry its namespace (`FfiConverterNetTypeFrame`, aliased
to `::net::uniffi::FfiConverterTypeFrame`) and never shadow a local
namesake. External types are qualified from the global namespace, error
and enum-struct checks look the type up in its own crate, the definition
sort ignores external types, and `strip_type_prefix` renames are keyed
by crate.

C: external types are supported. Their C names and converters are their
own crate's, whose header is included; every C name now carries the
namespace prefix unless it already starts with it, since crates share
one C scope.

Adds both namespaces crates to the C++ fixtures, with a C++ and a C test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
…-namespaces

# Conflicts:
#	uniffi/fixtures/namespaces/media/tests/test_generated_bindings.rs
@kixelated kixelated changed the title Namespaced external types in the UniFFI fork feat(uniffi): same-named types across crates in every generator Oct 24, 2026
@kixelated
kixelated marked this pull request as ready for review October 24, 2026 13:04
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T17:06:47.395461Z 8670b55 New commits
🔒 Security Review ✅ Completed 2026-10-24T17:08:10.182599Z 8670b55 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da5520b0b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Handle flat enums specially - lower to RustBuffer and extract int32
quote!(
final $(field_name(field, i))_buffer = $(field_ffi_converter_name(field)).lower($(field_name(field, i)));
final $(field_name(field, i))_buffer = $(field_ffi_converter_name(field, type_helper.get_ci())).lower($(field_name(field, i)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve an external enum's shape in its defining interface

When a local flat enum and an external data-carrying enum share a name, a variant containing the external enum is serialized incorrectly. The is_flat_enum helper still calls type_helper.get_enum(name), which looks up the local namesake, while the converter now correctly refers to the external enum. Consequently, allocation and writing reserve and copy only four bytes, discarding the external enum's payload; reading likewise passes only its discriminant to the external converter. Resolve the enum definition through its owning component, or use the converter's normal read/write/allocation methods without this special case.

AGENTS.md reference: AGENTS.md:L15-L15

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 899cc21: is_flat_enum now resolves the enum in the crate that defines it. The fixture gained a data-carrying net::Kind next to a flat media::Kind, both held by media::Track variants; the Dart test failed with a RangeError before the fix and passes after.

(written by Opus 5.5)

Comment on lines +48 to +49
if crates.len() == 1 && ci.get_type(name).is_none() {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Import all dependencies when Swift requires separate modules

When a library contains a name collision plus an unambiguous dependency from another crate, this early continue omits an import required by the separate-module layout. For example, if media defines Frame, uses net::Frame, and also uses extra::Other, the generated Swift imports net but neither imports extra nor qualifies Other. The updated Swift test runner splits every crate into its own module as soon as any collision requires an import, so Other and its converters are then out of scope. Determine the module layout across the components and include every external dependency's module when using the split layout, rather than importing only types whose names collide.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 899cc21: once any crate in the library needs its own Swift module, every crate's bindings import every crate they use, not only the ones they qualify. The layout is decided across components in apply_renames (separate_modules). A unit test with a third crate's unambiguous type fails on the old code and passes now.

(written by Opus 5.5)

kixelated and others added 2 commits October 24, 2026 07:02
…very crate in Swift's split layout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115AT2ue6x9cT5e5J9SZtCn
@kixelated

Copy link
Copy Markdown
Owner Author

Merging at 8670b55.

(written by Opus 5.5)

@kixelated
kixelated merged commit a12da04 into main Oct 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant