Skip to content

fix(uniffi): register the Bytes custom type once, in livekit-common - #1343

Open
pblazej wants to merge 5 commits into
mainfrom
blaze/uniffi-bytes-remote-type
Open

fix(uniffi): register the Bytes custom type once, in livekit-common#1343
pblazej wants to merge 5 commits into
mainfrom
blaze/uniffi-bytes-remote-type

Conversation

@pblazej

@pblazej pblazej commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

bytes::Bytes was registered with custom_type! in both livekit-uniffi and livekit-datatrack. A remote type can only be registered once per UniFFI component: each emits its own public typealias Bytes and FfiConverterTypeBytes, and cargo-swift compiles every component file into one Swift module — invalid redeclaration of 'Bytes'. UniFFI's own duplicated helpers coexist because they're fileprivate; custom_type! emits public, so they don't. Patched until now by a perl regex in swift-workarounds, which is deleted here.

livekit-common now owns the registration and every component borrows it:

uniffi::use_remote_type!(livekit_common::Bytes);

Owning a registration means being a component — custom_type! resolves crate::UniFfiTag, and bindgen rejects metadata belonging to no namespace (Unknown namespace for CustomType). So livekit-common gains an optional uniffi feature, enabled transitively by livekit-uniffi, and a third generated Swift file. That file had to be added to uniffi-swift.yml — the publish step enumerates sources rather than globbing, so an unlisted component silently ships a package that can't build. Costs 4,160 B on the cdylib (1,107,856 → 1,112,016), leaving 66 KiB under the iOS gate.

Verified: livekit-common and livekit-datatrack still build without the feature; the three-file Swift module compiles; client-sdk-swift main builds clean against the generated package, tests included; the xcframework carries all three headers in one framework modulemap.

Alternatives for the collision

Option Verdict
Keep the perl regex Fragile — matches literal generated formatting, so it silently no-ops when uniffi's Swift templates shift — and Swift-only, so Kotlin and Python kept shipping two declarations.
Let livekit-datatrack own it Free (80 B, no new component), but an unintuitive home for a generic byte buffer, and any future component would have to depend on livekit-datatrack to borrow it. Was the first version of this PR.
Collapse to one component — drop uniffi from livekit-datatrack and mirror its types with #[uniffi::remote] Removes the class outright and returns the duplication below. But ~150 lines of mirrored Rust, hand-written adapters for the two with_foreign traits, and dropping #[non_exhaustive] from two enums. Separate PR.

Upstream: #2933 (open) is this bug, and this PR takes the external-type route it proposes. #2126 (open) — no #[uniffi::remote(Trait)] in 0.31 or 0.32 — is why collapsing needs trait adapters. #2802 (open) is the same conflict across separate UniFFI libraries.

Alternatives for the duplication

Not addressed here. Each component re-emits the whole FFI runtime: ≈19 KB across three of them — 8.3 KB native and ~10.6 KB of Swift code+data, from two measured per-component deltas of ~4.2 KB and 5,287 B.

Option Verdict
Hoist the shared Swift runtime into one internal file Worse — prototyped at +14,592 B on the linked binary, because it destroys the per-file specializations that make the duplicate copies cheap. Buys ~5% compile time.
Wait for upstream to emit the common part once Asked for twice, declined twice. Not in flight.
Collapse to one component The only thing that actually returns it.

Upstream: #408 / #409 (closed) asked to eliminate re-declared Swift helpers and were resolved with the UNIFFI_SHARED_H guard plus fileprivate — duplicates left to coexist rather than removed. #2257 (closed) asked to combine the per-crate outputs; closed without a feature. #2153 (open) is the only live proposal that would share the runtime, and also documents the Python breakage: bindgen writes from . import livekit_datatrack into a flat out-dir with no __init__.py, giving ImportError: attempted relative import with no known parent package. #2930 (open) is the Swift → bindings-pipeline migration such a change would land on.

`bytes::Bytes` was registered with `custom_type!` in both livekit-uniffi and
livekit-datatrack. They are separate UniFFI components, so each emitted its own
`typealias Bytes` and `FfiConverterTypeBytes` — and cargo-swift compiles both
component files into one Swift module, so they collided.

Borrow datatrack's registration with `use_remote_type!` instead of adding a
second one. The type is then emitted once, in the component that owns it, which
makes the `swift-workarounds` perl task unnecessary — and that task has to go in
the same commit, since it would otherwise strip the only remaining definition.

Unlike the regex, this is compile-checked and also fixes Kotlin and Python,
which were still shipping two `Bytes` declarations.

Upstream: mozilla/uniffi-rs#2933

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pblazej
pblazej requested a review from ladvoc as a code owner August 20, 2026 11:00
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Changeset incomplete

This PR's changeset is missing version bumps for packages that are affected by the change. The following packages still require a bump:

  • livekit
  • livekit-api
  • livekit-data-stream
  • livekit-ffi

Already covered:

  • livekit-common (patch)
  • livekit-datatrack (patch)
  • livekit-uniffi (patch)

A package must be bumped when its own files change, and whenever a package it depends on is bumped (so downstream consumers get a matching release).

Click here to create a changeset for the missing packages

The link pre-populates a changeset file with patch bumps for the missing packages. You can also add them to your existing changeset. Edit the bump types as needed before committing.

If this change doesn't require a version bump, add the internal label to this PR.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@pblazej
pblazej requested a review from 1egoman August 20, 2026 11:13
Comment thread livekit-uniffi/src/common.rs Outdated
Comment on lines +17 to +23
// `Bytes` is a remote type, so each UniFFI component that registers it with
// `custom_type!` emits its own converter — and the two component Swift files are
// compiled into one module, so a second `typealias Bytes` fails to build
// ("invalid redeclaration of 'Bytes'"). Reuse livekit-datatrack's registration
// instead of adding a second one; the type is then emitted once, in the file that
// owns it. Upstream: https://github.com/mozilla/uniffi-rs/issues/2933
uniffi::use_remote_type!(livekit_datatrack::Bytes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My main concern with putting this in livekit-datatrack is that it's a somewhat unintuitive place for this definition to go. I'm also not sure how this would impact data streams v2 given the uniffi bindings will have the same problem (and a dependency between livekit-datatrack and livekit-data-stream sounds like a bad idea).

I realize it's a bit more work, but what do you think about doing something like defining this custom_type! in some third package which livekit-uniffi, livekit-datatrack, and data streams v2 uniffi once that is is merged can all pull in via uniffi::use_remote_type!(...)? I'm not sure what this third package would be called, but my initial thought is maybe it could be in livekit-common - that package could gain a uniffi feature which gets set through each layer when building livekit-uniffi.

Thoughts on this?

@pblazej pblazej Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Prototyped it — works, Bytes lands once in livekit_common.swift. But it has to be a full component: a hand-written UniFfiTag compiles, then bindgen refuses with Unknown namespace for CustomType. Cost is a third namespace — livekit_common.swift at 558 lines, 553 of them duplicated runtime, publishing one typealias Bytes = Data — plus +4,160 B on the cdylib.

#1286 doesn't hit this either: no new setup_scaffolding!(), it reuses this crate's registration. And it answers the general question the other way, hand-mirroring common::EncryptionType/ClientCapability into livekit-uniffi.

So it turns on whether livekit-common becomes a component — bigger than this PR, and a two-line follow-up either way.

@pblazej pblazej Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — pushed b46be14, livekit-common owns it now.

Verified: livekit-common and livekit-datatrack still build without the feature (the dep is optional, gated on uniffi), the three-file module compiles, client-sdk-swift main builds clean against the generated package with tests, and the xcframework carries all three headers in one modulemap. Cost is 4,160 B on the cdylib, 66 KiB under the iOS gate.

@ladvoc

ladvoc commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Wait, what happens if you simply do this:

uniffi::use_remote_type!(bytes::Bytes);

At least the docs for this macro imply this would work.

@pblazej

pblazej commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@ladvoc looks like it won't work:

error[E0425]: cannot find type `UniFfiTag` in crate `bytes`

The first path segment isn't the crate that defines the type — it's the crate whose registration you're borrowing. The macro expands to <Bytes as FfiConverter<bytes::UniFfiTag>>, and bytes isn't a UniFFI crate, so it has no UniFfiTag. The docs are genuinely misleading here: their example borrows from another uniffi crate, so the path reads like a plain type path. You need a crate that called setup_scaffolding!().

A remote type can only be registered once per UniFFI component, so owning the
registration in livekit-datatrack made every other component borrow from a crate
that has nothing to do with byte buffers. Register it in livekit-common instead,
which is where shared foundational types already live, and have each component
borrow it with `use_remote_type!`.

Owning a registration means being a component: `custom_type!` resolves
`crate::UniFfiTag`, and bindgen rejects type metadata belonging to no namespace
("Unknown namespace for CustomType"). So livekit-common gains an optional
`uniffi` feature, enabled transitively by livekit-uniffi, and a third generated
Swift file. That file has to be listed in uniffi-swift.yml — the publish step
enumerates sources rather than globbing, so an unlisted component silently ships
a package that cannot build.

Costs 4,160 bytes on the release cdylib (1,107,856 -> 1,112,016), leaving 66 KiB
under the iOS size gate.

Verified: livekit-common and livekit-datatrack still build without the feature;
the three-file Swift module compiles; client-sdk-swift main builds clean against
the generated package, tests included; the xcframework carries all three headers
in one framework modulemap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pblazej pblazej changed the title fix(uniffi): reuse livekit-datatrack's Bytes converter instead of re-registering it fix(uniffi): register the Bytes custom type once, in livekit-common Aug 21, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

pblazej and others added 3 commits August 21, 2026 08:57
The publish step enumerated each generated source, so a new UniFFI component
would have shipped a package missing its types — and the action only aborts on
listed-but-absent files, never on an unlisted one.

livekit/publish-xcframework-action#5 makes `files` sources path patterns, the
same convention as actions/upload-artifact and softprops/action-gh-release, with
a trailing-slash destination meaning "directory, keep basenames". Match
`Sources/<name>/*.swift` so adding a component needs no workflow change; a
pattern matching nothing still fails the job.

Pinned to that PR's head commit; repin to the merge commit once it lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It is only used by `ffi_types`, which is gated on the `uniffi` feature, so a
consumer that doesn't want the FFI registrations shouldn't take the dependency
edge. Without the feature the crate's only direct dependency is livekit-protocol
again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
livekit/publish-xcframework-action#5 is merged; pin the merge commit rather than
the PR branch head.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@1egoman 1egoman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice, I'm a fan of the livekit-common approach!

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.

3 participants