From e1172c9633ae75e4941219a9ec1bdc8d2ece96fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:00:00 +0200 Subject: [PATCH 1/6] fix(uniffi): reuse livekit-datatrack's Bytes converter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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: https://github.com/mozilla/uniffi-rs/issues/2933 Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/reuse_datatrack_bytes_converter.md | 5 +++++ livekit-uniffi/Makefile.toml | 21 +------------------ livekit-uniffi/src/common.rs | 8 ++++++- 3 files changed, 13 insertions(+), 21 deletions(-) create mode 100644 .changeset/reuse_datatrack_bytes_converter.md diff --git a/.changeset/reuse_datatrack_bytes_converter.md b/.changeset/reuse_datatrack_bytes_converter.md new file mode 100644 index 000000000..69330802b --- /dev/null +++ b/.changeset/reuse_datatrack_bytes_converter.md @@ -0,0 +1,5 @@ +--- +livekit-uniffi: patch +--- + +Reuse `livekit-datatrack`'s `Bytes` converter instead of registering a second one, so UniFFI emits it once and the post-generation Swift workaround is no longer needed diff --git a/livekit-uniffi/Makefile.toml b/livekit-uniffi/Makefile.toml index ec3e57746..faca84ee4 100644 --- a/livekit-uniffi/Makefile.toml +++ b/livekit-uniffi/Makefile.toml @@ -475,24 +475,6 @@ mkdir -p "${out_dir}" mv "${SPM_NAME}" "${out_dir}" """ -# Post-generation Swift workarounds for issues cargo-swift / the templates don't handle. -# Currently: the `Bytes` custom type is registered in both crates, so uniffi emits its -# converter in both component Swift files, which collide in one module ("invalid -# redeclaration of 'Bytes'"). Drop the duplicate from livekit_datatrack.swift. -# Upstream: https://github.com/mozilla/uniffi-rs/issues/2933. Idempotent. -[tasks.swift-workarounds] -private = true -script_runner = "@shell" -script = ''' -file="${PACKAGES_DIR}/swift/${SPM_NAME}/Sources/${SPM_NAME}/livekit_datatrack.swift" -if [ ! -f "$file" ]; then - echo "swift-workarounds: $file not found, skipping" - exit 0 -fi -perl -0pi -e 's{public typealias Bytes = Data.*?public func FfiConverterTypeBytes_lower\(_ value: Bytes\) -> RustBuffer \{\n return FfiConverterTypeBytes\.lower\(value\)\n\}}{// cargo-make swift-workarounds: duplicate Bytes converter removed (kept in livekit_uniffi.swift)}s' "$file" -echo "swift-workarounds: ensured single Bytes converter in $file" -''' - [tasks.swift-package-flow] private = true dependencies = [ @@ -501,8 +483,7 @@ dependencies = [ "swift-check-size", "swift-zip-xcframework", "swift-generate-manifest", - "swift-move-to-packages", - "swift-workarounds" + "swift-move-to-packages" ] [tasks.swift-package] diff --git a/livekit-uniffi/src/common.rs b/livekit-uniffi/src/common.rs index 6ff66a67e..95473761b 100644 --- a/livekit-uniffi/src/common.rs +++ b/livekit-uniffi/src/common.rs @@ -14,4 +14,10 @@ use bytes::Bytes; -uniffi::custom_type!(Bytes, Vec, { remote }); +// `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); From b46be142e08d3d345cd62f804e95703134006739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:46:46 +0200 Subject: [PATCH 2/6] refactor(uniffi): move the Bytes registration to livekit-common MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .changeset/reuse_datatrack_bytes_converter.md | 4 ++- .github/workflows/uniffi-swift.yml | 4 +++ Cargo.lock | 4 +++ livekit-common/Cargo.toml | 7 +++++ livekit-common/src/ffi_types.rs | 29 +++++++++++++++++++ livekit-common/src/lib.rs | 8 +++++ livekit-common/uniffi.toml | 4 +++ livekit-datatrack/Cargo.toml | 3 +- livekit-datatrack/src/e2ee.rs | 2 +- livekit-uniffi/Cargo.toml | 1 + livekit-uniffi/src/common.rs | 12 ++++---- 11 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 livekit-common/src/ffi_types.rs create mode 100644 livekit-common/uniffi.toml diff --git a/.changeset/reuse_datatrack_bytes_converter.md b/.changeset/reuse_datatrack_bytes_converter.md index 69330802b..bfa725e12 100644 --- a/.changeset/reuse_datatrack_bytes_converter.md +++ b/.changeset/reuse_datatrack_bytes_converter.md @@ -1,5 +1,7 @@ --- livekit-uniffi: patch +livekit-datatrack: patch +livekit-common: patch --- -Reuse `livekit-datatrack`'s `Bytes` converter instead of registering a second one, so UniFFI emits it once and the post-generation Swift workaround is no longer needed +Register the `Bytes` UniFFI custom type once in `livekit-common` and borrow it from each component with `uniffi::use_remote_type!`, so the converter is emitted once and the post-generation Swift workaround is no longer needed diff --git a/.github/workflows/uniffi-swift.yml b/.github/workflows/uniffi-swift.yml index 68a594579..b9e86c5d8 100644 --- a/.github/workflows/uniffi-swift.yml +++ b/.github/workflows/uniffi-swift.yml @@ -96,7 +96,11 @@ jobs: ${{ env.OUTPUT_DIR }}/${{ env.SPM_NAME }}.podspec:${{ env.SPM_NAME }}.podspec ${{ env.OUTPUT_DIR }}/LICENSE:LICENSE ${{ env.OUTPUT_DIR }}/PrivacyInfo.xcprivacy:PrivacyInfo.xcprivacy + # One entry per UniFFI component (each `setup_scaffolding!()` crate gets its own + # generated file). Add a line here when a component is added, or the published + # package will be missing types and fail to build for consumers. ${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/livekit_uniffi.swift:Sources/${{ env.SPM_NAME }}/livekit_uniffi.swift + ${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/livekit_common.swift:Sources/${{ env.SPM_NAME }}/livekit_common.swift ${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/livekit_datatrack.swift:Sources/${{ env.SPM_NAME }}/livekit_datatrack.swift token: ${{ secrets.UNIFFI_XCFRAMEWORK_PAT }} pr-body: | diff --git a/Cargo.lock b/Cargo.lock index 0aeee4884..5f910ec78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3963,7 +3963,9 @@ dependencies = [ name = "livekit-common" version = "0.1.1" dependencies = [ + "bytes", "livekit-protocol", + "uniffi", ] [[package]] @@ -3999,6 +4001,7 @@ dependencies = [ "futures-core", "futures-util", "indexmap 2.14.0", + "livekit-common", "livekit-protocol", "livekit-runtime", "log", @@ -4104,6 +4107,7 @@ dependencies = [ "camino", "futures-util", "livekit-api", + "livekit-common", "livekit-datatrack", "livekit-protocol", "log", diff --git a/livekit-common/Cargo.toml b/livekit-common/Cargo.toml index a5bc3f5f9..9559cd477 100644 --- a/livekit-common/Cargo.toml +++ b/livekit-common/Cargo.toml @@ -9,3 +9,10 @@ repository.workspace = true [dependencies] livekit-protocol = { workspace = true } +bytes = { workspace = true } +uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns"], optional = true } + +[features] +# Exposes this crate's shared FFI type registrations. Enabled transitively by +# livekit-uniffi so every component borrows one converter per type. +uniffi = ["dep:uniffi"] diff --git a/livekit-common/src/ffi_types.rs b/livekit-common/src/ffi_types.rs new file mode 100644 index 000000000..c530ac5f7 --- /dev/null +++ b/livekit-common/src/ffi_types.rs @@ -0,0 +1,29 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Shared UniFFI type registrations. +//! +//! A remote type such as [`bytes::Bytes`] can only be registered once per UniFFI component: +//! `custom_type!` emits a `public` converter per component, and the generated Swift files are +//! compiled into a single module, so a second registration fails to build. Registering it here +//! and having each component borrow it with `uniffi::use_remote_type!` keeps exactly one +//! declaration no matter how many crates need the type. +//! +//! Owning a registration requires being a UniFFI component: `custom_type!` needs +//! `crate::UniFfiTag`, and bindgen rejects type metadata that belongs to no namespace +//! (`Unknown namespace for CustomType`). Hence the `setup_scaffolding!` in `lib.rs`. + +use bytes::Bytes; + +uniffi::custom_type!(Bytes, Vec, { remote }); diff --git a/livekit-common/src/lib.rs b/livekit-common/src/lib.rs index 550af50f2..ed5dc08b3 100644 --- a/livekit-common/src/lib.rs +++ b/livekit-common/src/lib.rs @@ -22,6 +22,14 @@ use livekit_protocol as proto; mod enum_dispatch; +// Must sit at the crate root: it defines `crate::UniFfiTag`, which the registrations in +// `ffi_types` resolve against. +#[cfg(feature = "uniffi")] +uniffi::setup_scaffolding!(); + +#[cfg(feature = "uniffi")] +mod ffi_types; + // ------------------------------------------------------------------------------------------------- // Client protocol // ------------------------------------------------------------------------------------------------- diff --git a/livekit-common/uniffi.toml b/livekit-common/uniffi.toml new file mode 100644 index 000000000..a516eec17 --- /dev/null +++ b/livekit-common/uniffi.toml @@ -0,0 +1,4 @@ +[bindings.swift] +# Matches the Rust convention used by the other components; the cargo-swift +# fork folds every component's header into the single RustLiveKitUniFFI framework. +ffi_module_name = "RustLiveKitCommon" diff --git a/livekit-datatrack/Cargo.toml b/livekit-datatrack/Cargo.toml index ebacd04cb..1caf9968c 100644 --- a/livekit-datatrack/Cargo.toml +++ b/livekit-datatrack/Cargo.toml @@ -9,6 +9,7 @@ repository.workspace = true [dependencies] livekit-protocol = { workspace = true } +livekit-common = { workspace = true, optional = true } livekit-runtime = { workspace = true, features = ["tokio"] } log = { workspace = true } thiserror = { workspace = true } @@ -25,7 +26,7 @@ uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns"], optional indexmap = "2" [features] -uniffi = ["dep:uniffi"] +uniffi = ["dep:uniffi", "dep:livekit-common", "livekit-common/uniffi"] __fuzz = ["dep:fake"] [dev-dependencies] diff --git a/livekit-datatrack/src/e2ee.rs b/livekit-datatrack/src/e2ee.rs index b1fdd828b..736e88038 100644 --- a/livekit-datatrack/src/e2ee.rs +++ b/livekit-datatrack/src/e2ee.rs @@ -76,7 +76,7 @@ pub trait DecryptionProvider: Send + Sync + Debug { } #[cfg(feature = "uniffi")] -uniffi::custom_type!(Bytes, Vec, { remote }); +uniffi::use_remote_type!(livekit_common::Bytes); #[cfg(feature = "uniffi")] uniffi::custom_type!(InitializationVector, Vec, { diff --git a/livekit-uniffi/Cargo.toml b/livekit-uniffi/Cargo.toml index 665f2ca55..0054996f6 100644 --- a/livekit-uniffi/Cargo.toml +++ b/livekit-uniffi/Cargo.toml @@ -13,6 +13,7 @@ publish = false [dependencies] livekit-protocol = { workspace = true } +livekit-common = { workspace = true, features = ["uniffi"] } livekit-api = { workspace = true, default-features = false, features = ["access-token"] } livekit-datatrack = { workspace = true, features = ["uniffi"] } uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns", "tokio"] } diff --git a/livekit-uniffi/src/common.rs b/livekit-uniffi/src/common.rs index 95473761b..e7d1fa5f0 100644 --- a/livekit-uniffi/src/common.rs +++ b/livekit-uniffi/src/common.rs @@ -14,10 +14,8 @@ use bytes::Bytes; -// `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); +// `Bytes` is registered once in livekit-common so every component that needs it borrows the +// same converter; registering it here as well would emit a second `public typealias Bytes` +// into this component's Swift file, and both files compile into one module. +// Upstream: https://github.com/mozilla/uniffi-rs/issues/2933 +uniffi::use_remote_type!(livekit_common::Bytes); From 5eccef2bd549a8b1d697674444f2c430a7007032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:57:15 +0200 Subject: [PATCH 3/6] ci(uniffi): match generated Swift sources instead of listing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//*.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) --- .github/workflows/uniffi-swift.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/uniffi-swift.yml b/.github/workflows/uniffi-swift.yml index b9e86c5d8..2839c20e4 100644 --- a/.github/workflows/uniffi-swift.yml +++ b/.github/workflows/uniffi-swift.yml @@ -85,7 +85,8 @@ jobs: - name: Publish to hosting repo if: ${{ !inputs.dry_run }} - uses: livekit/publish-xcframework-action@200ee4984ef1b68068c0b3c4e87e798988730c90 # main @ fix: abort on missing source files (#3) + # TODO: repin to the merge commit once livekit/publish-xcframework-action#5 lands. + uses: livekit/publish-xcframework-action@94cb0ea28a25e18469c4a3e43c7f162a84da2169 # feat/glob-sources @ feat: allow path patterns as file sources (#5) with: xcframework-zip: ${{ env.OUTPUT_DIR }}/Rust${{ env.SPM_NAME }}.xcframework.zip version: ${{ inputs.version }} @@ -96,12 +97,9 @@ jobs: ${{ env.OUTPUT_DIR }}/${{ env.SPM_NAME }}.podspec:${{ env.SPM_NAME }}.podspec ${{ env.OUTPUT_DIR }}/LICENSE:LICENSE ${{ env.OUTPUT_DIR }}/PrivacyInfo.xcprivacy:PrivacyInfo.xcprivacy - # One entry per UniFFI component (each `setup_scaffolding!()` crate gets its own - # generated file). Add a line here when a component is added, or the published - # package will be missing types and fail to build for consumers. - ${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/livekit_uniffi.swift:Sources/${{ env.SPM_NAME }}/livekit_uniffi.swift - ${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/livekit_common.swift:Sources/${{ env.SPM_NAME }}/livekit_common.swift - ${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/livekit_datatrack.swift:Sources/${{ env.SPM_NAME }}/livekit_datatrack.swift + # UniFFI emits one source per component, so match rather than list them: + # adding a component must not require a workflow change. + ${{ env.OUTPUT_DIR }}/Sources/${{ env.SPM_NAME }}/*.swift:Sources/${{ env.SPM_NAME }}/ token: ${{ secrets.UNIFFI_XCFRAMEWORK_PAT }} pr-body: | Source tag: `${{ inputs.tag_name }}` From 4f3e5120102810e5b9a2abbcce2c6b844dba775f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Fri, 21 Aug 2026 09:00:30 +0200 Subject: [PATCH 4/6] fix(common): make bytes optional 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) --- livekit-common/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit-common/Cargo.toml b/livekit-common/Cargo.toml index 9559cd477..0e2d03f44 100644 --- a/livekit-common/Cargo.toml +++ b/livekit-common/Cargo.toml @@ -9,10 +9,10 @@ repository.workspace = true [dependencies] livekit-protocol = { workspace = true } -bytes = { workspace = true } +bytes = { workspace = true, optional = true } uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns"], optional = true } [features] # Exposes this crate's shared FFI type registrations. Enabled transitively by # livekit-uniffi so every component borrows one converter per type. -uniffi = ["dep:uniffi"] +uniffi = ["dep:uniffi", "dep:bytes"] From 637262f481bcfe9007f017511490edd8eedc8fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Fri, 21 Aug 2026 09:27:38 +0200 Subject: [PATCH 5/6] ci(uniffi): repin publish action to main 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) --- .github/workflows/uniffi-swift.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/uniffi-swift.yml b/.github/workflows/uniffi-swift.yml index 2839c20e4..2ddc97b6a 100644 --- a/.github/workflows/uniffi-swift.yml +++ b/.github/workflows/uniffi-swift.yml @@ -85,8 +85,7 @@ jobs: - name: Publish to hosting repo if: ${{ !inputs.dry_run }} - # TODO: repin to the merge commit once livekit/publish-xcframework-action#5 lands. - uses: livekit/publish-xcframework-action@94cb0ea28a25e18469c4a3e43c7f162a84da2169 # feat/glob-sources @ feat: allow path patterns as file sources (#5) + uses: livekit/publish-xcframework-action@a1bc3de909f9c0a2b7f92172a95d6dd35cb20b1a # main @ feat: allow path patterns as file sources (#5) with: xcframework-zip: ${{ env.OUTPUT_DIR }}/Rust${{ env.SPM_NAME }}.xcframework.zip version: ${{ inputs.version }} From 075ad1f4c996fcaf7c6adba7bd24d52ad9249a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82a=C5=BCej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:45:34 +0200 Subject: [PATCH 6/6] Create fix_uniffi_register_the_bytes_custom_type_once_in_livekit_co.md --- ...i_register_the_bytes_custom_type_once_in_livekit_co.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/fix_uniffi_register_the_bytes_custom_type_once_in_livekit_co.md diff --git a/.changeset/fix_uniffi_register_the_bytes_custom_type_once_in_livekit_co.md b/.changeset/fix_uniffi_register_the_bytes_custom_type_once_in_livekit_co.md new file mode 100644 index 000000000..3198bd4fb --- /dev/null +++ b/.changeset/fix_uniffi_register_the_bytes_custom_type_once_in_livekit_co.md @@ -0,0 +1,8 @@ +--- +livekit: patch +livekit-api: patch +livekit-data-stream: patch +livekit-ffi: patch +--- + +fix(uniffi): register the Bytes custom type once, in livekit-common - #1343 (@pblazej)