diff --git a/.changes/uniffi-rust-core-dev-wiring b/.changes/uniffi-rust-core-dev-wiring new file mode 100644 index 000000000..988c69b75 --- /dev/null +++ b/.changes/uniffi-rust-core-dev-wiring @@ -0,0 +1 @@ +patch type="added" "Wire in the livekit_uniffi Rust core behind a native-only facade, delivered as a bundled cdylib via Native Assets" diff --git a/AGENTS.md b/AGENTS.md index f77196e0e..27e4f57cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,28 @@ CI (`build.yaml`) runs all of the above plus example-app builds for every platfo Web/native divergence is handled with conditional imports (e.g. `track/processor_native.dart` vs `processor_web.dart`) — new platform-specific code should follow that pattern. +## The Rust core (`livekit_uniffi`) + +`lib/src/uniffi/` wraps `livekit_uniffi`, a Dart package generated from the `livekit-uniffi` crate in the sibling `rust-sdks` repo. It reaches Rust through Dart's Native Assets: the package's `hook/build.dart` bundles a `cdylib` into the host app and the generated bindings call into it with `@Native`. This is why the SDK requires Flutter >= 3.38 / Dart >= 3.10. + +There is no dynamic library to load on the web, so `uniffi.dart` splits native/web the same way the rest of the SDK does. **`uniffi_io.dart` is the only file allowed to import `package:livekit_uniffi/...`** — importing it from anywhere reachable on web pulls `dart:ffi` into a web compile and breaks `flutter build web`/`--wasm`. Guard calls with `LiveKitUniffi.isAvailable`. + +### Local development loop + +`livekit_uniffi` is not on pub.dev yet, so both `pubspec.yaml` and `example/pubspec.yaml` override it to a path in a sibling `rust-sdks` checkout (overrides don't propagate from a dependency, hence both). To produce or refresh it: + +```sh +cd ../rust-sdks/livekit-uniffi +cargo make dart-package # generates packages/dart/: bindings, pubspec, build hook, host cdylib +cd - +flutter pub get +flutter test test/uniffi/ # smoke test: calls buildVersion() across the FFI boundary +``` + +Requires `cargo-make`, `protoc` and `tera`. Re-run `cargo make dart-package` whenever the crate's exported surface changes — the build hook tracks the copied library, so a stale one won't be silently reused. + +Two things to know about that hook: it picks the locally built library purely on target *OS*, not architecture, so a host build can be bundled into an iOS or Android build by mistake — verify the desktop target first when debugging. And its download mode (used when no local library is present) fetches `build-.zip` from a `livekit-uniffi` GitHub release; no release currently carries those assets, so download mode fails until the `cdylib` job is re-enabled in `rust-sdks`. + ## Common pitfalls (from issue history) - `flutter_webrtc` is pinned to an exact version on purpose: livekit_client and flutter_webrtc must agree on the same WebRTC-SDK native pods, and mismatches break user builds (CocoaPods conflicts). Bump it only in sync with a matching WebRTC-SDK version. diff --git a/analysis_options.yaml b/analysis_options.yaml index b7cb2942b..a19eddbd8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -22,12 +22,14 @@ analyzer: avoid_print: ignore deprecated_member_use_from_same_package: ignore - # Exclude protobuf files + # Exclude generated files: protobuf, and json_serializable output. Neither is + # hand-edited, so lint hits there can only be fixed by changing the generator. exclude: - "**/*.pb.dart" - "**/*.pbenum.dart" - "**/*.pbjson.dart" - "**/*.pbserver.dart" + - "**/*.g.dart" # - 'web/*.dart' # Xcode vendors Swift package checkouts under build when Swift Package # Manager is enabled and this package ships Package.swift. diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 38fd761a1..44bbed489 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -25,6 +25,13 @@ dependencies: livekit_client: path: ../ +dependency_overrides: + # Overrides do not propagate from a dependency, so livekit_client's override of + # livekit_uniffi has to be repeated here. Drop both once it is on pub.dev. + livekit_uniffi: + path: ../../rust-sdks/livekit-uniffi/packages/dart + + dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/src/uniffi/uniffi.dart b/lib/src/uniffi/uniffi.dart new file mode 100644 index 000000000..f5716c96b --- /dev/null +++ b/lib/src/uniffi/uniffi.dart @@ -0,0 +1,45 @@ +// 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. + +import 'uniffi_io.dart' if (dart.library.js_interop) 'uniffi_web.dart' as impl; + +/// Facade over the Rust core exposed by the `livekit_uniffi` package. +/// +/// `livekit_uniffi` reaches Rust through Dart's Native Assets: its build hook +/// bundles a `cdylib` into the host app and the generated bindings call into it +/// with `@Native`. None of that exists on the web, where there is no dynamic +/// library to load, so every entry point here is split native/web through the +/// same conditional-import pattern the rest of the SDK uses (see +/// `support/platform.dart`). Web builds must never reach the generated +/// bindings -- importing them at all would break `dart compile js`/`wasm`. +/// +/// Callers get [isAvailable] to branch on, and platform-specific code paths +/// stay out of the public API surface. +abstract final class LiveKitUniffi { + /// Whether the Rust core can be called on this platform. + /// + /// False on web. Every other member throws [UnsupportedError] when this is + /// false, rather than returning a silently wrong value. + static bool get isAvailable => impl.isAvailable; + + /// Version string reported by the Rust core. + /// + /// The simplest possible round trip -- a synchronous, argument-free call + /// returning a string -- so it doubles as the smoke test that the whole + /// chain is wired up: build hook resolved the library, `@Native` bound the + /// symbol, and a value came back across the FFI boundary. + /// + /// Throws [UnsupportedError] on web. + static String get buildVersion => impl.buildVersion(); +} diff --git a/lib/src/uniffi/uniffi_io.dart b/lib/src/uniffi/uniffi_io.dart new file mode 100644 index 000000000..983c5aaf2 --- /dev/null +++ b/lib/src/uniffi/uniffi_io.dart @@ -0,0 +1,23 @@ +// 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. + +import 'package:livekit_uniffi/livekit_uniffi.dart' as uniffi; + +/// Native implementation of [LiveKitUniffi]. See `uniffi.dart`. +/// +/// This is the only file in the SDK that may import the generated bindings: +/// the conditional import in `uniffi.dart` keeps it out of web builds. +const bool isAvailable = true; + +String buildVersion() => uniffi.buildVersion(); diff --git a/lib/src/uniffi/uniffi_web.dart b/lib/src/uniffi/uniffi_web.dart new file mode 100644 index 000000000..db2ab1968 --- /dev/null +++ b/lib/src/uniffi/uniffi_web.dart @@ -0,0 +1,27 @@ +// 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. + +/// Web implementation of [LiveKitUniffi]. See `uniffi.dart`. +/// +/// Native Assets bundles a `cdylib`, which the web has no way to load, so the +/// Rust core is simply absent here. This file deliberately does not import +/// `package:livekit_uniffi/...` -- doing so would pull `dart:ffi` into a web +/// compile and fail the build. +const bool isAvailable = false; + +Never buildVersion() => throw UnsupportedError( + 'LiveKitUniffi.buildVersion is not available on web: the Rust core is ' + 'delivered as a native library. Guard calls with ' + 'LiveKitUniffi.isAvailable.', +); diff --git a/pubspec.lock b/pubspec.lock index 86f4871ee..9f16035c2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "13.0.0" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" args: dependency: transitive description: @@ -432,6 +440,13 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + livekit_uniffi: + dependency: "direct main" + description: + path: "../rust-sdks/livekit-uniffi/packages/dart" + relative: true + source: path + version: "0.1.8" logger: dependency: transitive description: @@ -616,6 +631,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e + url: "https://pub.dev" + source: hosted + version: "6.5.2" protobuf: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1fb555319..0936c3587 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,17 @@ dependencies: flutter_webrtc: 1.6.0 dart_webrtc: ^1.8.0 + # Rust core (livekit-uniffi), delivered as a bundled cdylib via Native Assets. + # Native platforms only — see lib/src/uniffi/. + livekit_uniffi: ^0.1.7 + +# livekit_uniffi is not published to pub.dev yet, so it resolves out of a sibling +# rust-sdks checkout produced by `cargo make dart-package`. See AGENTS.md. Drop +# this once the package is published. +dependency_overrides: + livekit_uniffi: + path: ../rust-sdks/livekit-uniffi/packages/dart + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/uniffi/uniffi_test.dart b/test/uniffi/uniffi_test.dart new file mode 100644 index 000000000..b0f6df841 --- /dev/null +++ b/test/uniffi/uniffi_test.dart @@ -0,0 +1,40 @@ +// 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. + +@TestOn('vm') +library; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:livekit_client/src/uniffi/uniffi.dart'; + +void main() { + // Exercises the whole delivery chain rather than any particular API: the + // build hook resolved a cdylib for this target, Native Assets bundled it, + // `@Native` bound the symbol, and a value crossed back from Rust. If the + // bindgen or the hook regresses, this is what fails first. + group('livekit_uniffi', () { + test('is available on native platforms', () { + expect(LiveKitUniffi.isAvailable, isTrue); + }); + + test('buildVersion returns the Rust core version', () { + final version = LiveKitUniffi.buildVersion; + expect(version, isNotEmpty); + // The crate stamps its own semver, so assert the shape rather than a + // literal that would need bumping on every livekit-uniffi release. + expect(version, matches(RegExp(r'^\d+\.\d+\.\d+'))); + }); + }); +}