diff --git a/.changeset/reuse_datatrack_bytes_converter.md b/.changeset/reuse_datatrack_bytes_converter.md new file mode 100644 index 000000000..bfa725e12 --- /dev/null +++ b/.changeset/reuse_datatrack_bytes_converter.md @@ -0,0 +1,7 @@ +--- +livekit-uniffi: patch +livekit-datatrack: patch +livekit-common: patch +--- + +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..2ddc97b6a 100644 --- a/.github/workflows/uniffi-swift.yml +++ b/.github/workflows/uniffi-swift.yml @@ -85,7 +85,7 @@ jobs: - name: Publish to hosting repo if: ${{ !inputs.dry_run }} - uses: livekit/publish-xcframework-action@200ee4984ef1b68068c0b3c4e87e798988730c90 # main @ fix: abort on missing source files (#3) + 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 }} @@ -96,8 +96,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 - ${{ 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_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 }}` 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..0e2d03f44 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, 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", "dep:bytes"] 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/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..e7d1fa5f0 100644 --- a/livekit-uniffi/src/common.rs +++ b/livekit-uniffi/src/common.rs @@ -14,4 +14,8 @@ use bytes::Bytes; -uniffi::custom_type!(Bytes, Vec, { remote }); +// `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);