diff --git a/.github/scripts/check_no_private_symbols.py b/.github/scripts/check_no_private_symbols.py index d5224be0..f73645f8 100755 --- a/.github/scripts/check_no_private_symbols.py +++ b/.github/scripts/check_no_private_symbols.py @@ -14,7 +14,7 @@ # limitations under the License. # """ -Verify that liblivekit's exported ABI does not leak private dependency symbols. +Verify that liblivekit's exported ABI does not leak private implementation symbols. The LiveKit SDK statically links several private dependencies (spdlog, fmt, google::protobuf, absl, nlohmann/json). When those symbols escape the dynamic symbol table @@ -23,6 +23,10 @@ rcl_logging_spdlog ABI-clashing with our vendored spdlog and crashing inside spdlog::pattern_formatter). +The generated UniFFI C++ bindings are also private implementation details. +Their generated API and runtime symbols must remain hidden behind the SDK's +public ABI. + This script lists exported defined symbols from the supplied shared library using the platform-appropriate tool and fails (exit code 1) if any of them match a forbidden pattern. @@ -53,6 +57,10 @@ "google::protobuf", "absl::", "nlohmann::", + # Generated UniFFI C++ binding API and runtime implementation. + "livekit_ffi::", + "uniffi::", + "uniffi_", ] MAX_REPORTED_LEAKS = 20 @@ -237,7 +245,7 @@ def main(argv: list[str]) -> int: "(set LIVEKIT_SYMBOL_CHECK_VERBOSE=1 to see all)") print( - "\nliblivekit must not re-export private dependency symbols.\n" + "\nliblivekit must not re-export private dependency or UniFFI implementation symbols.\n" "If you intentionally added a public symbol that triggered this, mark\n" "it with LIVEKIT_API in include/livekit/visibility.h and rebuild.\n" ) diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml index 5ba316bc..933203ff 100644 --- a/.github/workflows/cpp-tools.yml +++ b/.github/workflows/cpp-tools.yml @@ -75,12 +75,12 @@ jobs: llvm-dev libclang-dev clang \ libssl-dev libcurl4-openssl-dev wget ca-certificates gnupg - - name: Install clang-tidy 19 (for ExcludeHeaderFilterRegex support) + - name: Install clang-tidy 19 and clang 21 run: | set -eux # Ubuntu 24.04 apt ships clang-tidy 18, which doesn't understand # ExcludeHeaderFilterRegex (added in 19). Pull clang-tidy 19 from - # the upstream LLVM apt repository and pin the unversioned names. + # the upstream LLVM apt repository. libwebrtc requires clang 21+. sudo install -m 0755 -d /etc/apt/keyrings wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ | sudo tee /etc/apt/keyrings/llvm.asc >/dev/null @@ -88,8 +88,10 @@ jobs: codename=$(lsb_release -cs) echo "deb [signed-by=/etc/apt/keyrings/llvm.asc] http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-19 main" \ | sudo tee /etc/apt/sources.list.d/llvm-19.list >/dev/null + echo "deb [signed-by=/etc/apt/keyrings/llvm.asc] http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-21 main" \ + | sudo tee /etc/apt/sources.list.d/llvm-21.list >/dev/null sudo apt-get update - sudo apt-get install -y clang-tidy-19 clang-tools-19 + sudo apt-get install -y clang-tidy-19 clang-tools-19 clang-21 libclang-21-dev sudo ln -sf /usr/bin/clang-tidy-19 /usr/local/bin/clang-tidy sudo ln -sf /usr/bin/run-clang-tidy-19 /usr/local/bin/run-clang-tidy clang-tidy --version @@ -104,8 +106,9 @@ jobs: run: | echo "CXXFLAGS=-Wno-deprecated-declarations" >> "$GITHUB_ENV" echo "CFLAGS=-Wno-deprecated-declarations" >> "$GITHUB_ENV" - LLVM_VERSION=$(llvm-config --version | cut -d. -f1) - echo "LIBCLANG_PATH=/usr/lib/llvm-${LLVM_VERSION}/lib" >> "$GITHUB_ENV" + echo "CC=/usr/bin/clang-21" >> "$GITHUB_ENV" + echo "CXX=/usr/bin/clang++-21" >> "$GITHUB_ENV" + echo "LIBCLANG_PATH=/usr/lib/llvm-21/lib" >> "$GITHUB_ENV" - name: Configure compilation database run: cmake --preset linux-release @@ -113,6 +116,9 @@ jobs: - name: Generate protobuf headers run: cmake --build build-release --target livekit_proto + - name: Generate UniFFI C++ bindings + run: cmake --build build-release --target generate_livekit_ffi_uniffi_cpp + - name: Run clang-tidy env: TIDY_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/AGENTS.md b/AGENTS.md index 55da0430..45c875eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -234,8 +234,9 @@ The exported ABI is enforced by `.github/scripts/check_no_private_symbols.py`, run from the `make-release.yml` "Symbol leak check" CI step so a leak blocks the release build itself (it does not run on regular pushes/PRs). The script fails if `nm`/`dumpbin` reports any exported symbol matching a forbidden -substring (currently `spdlog::`, `fmt::v`, `google::protobuf`, `absl::`). To -run it locally, point it at the built shared library: +substring (including private dependency namespaces and generated UniFFI +symbols such as `livekit_ffi::`, `uniffi::`, and `uniffi_`). To run it locally, +point it at the built shared library: ```bash python3 .github/scripts/check_no_private_symbols.py \ diff --git a/CMakeLists.txt b/CMakeLists.txt index c9347c62..7fe6943d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,7 @@ set(FFI_PROTO_FILES ${FFI_PROTO_DIR}/track.proto ${FFI_PROTO_DIR}/video_frame.proto ${FFI_PROTO_DIR}/audio_frame.proto + ${FFI_PROTO_DIR}/capture.proto ${FFI_PROTO_DIR}/e2ee.proto ${FFI_PROTO_DIR}/stats.proto ${FFI_PROTO_DIR}/data_stream.proto @@ -269,6 +270,10 @@ endif() message(STATUS \"[run_cargo.cmake] CFG=\${CFG} CARGO=\${CARGO} PROTOC=\${PROTOC_PATH}\") set(ENV{PROTOC} \"\${PROTOC_PATH}\") +if(DEFINED CARGO_TARGET_DIR AND NOT CARGO_TARGET_DIR STREQUAL \"\") + set(ENV{CARGO_TARGET_DIR} \"\${CARGO_TARGET_DIR}\") +endif() + if(DEFINED GCC_LIB_DIR AND NOT GCC_LIB_DIR STREQUAL \"\") set(ENV{RUSTFLAGS} \"-L \${GCC_LIB_DIR} \$ENV{RUSTFLAGS}\") set(ENV{LD_LIBRARY_PATH} \"\${GCC_LIB_DIR}:\$ENV{LD_LIBRARY_PATH}\") @@ -368,6 +373,8 @@ add_custom_target(build_rust_ffi DEPENDS "${RUST_LIB_DEBUG}" "${RUST_LIB_RELEASE}" ) +include(uniffi_cpp) + # Note: protozero_plugin.o removal is no longer needed since we use dynamic libraries on Unix add_library(livekit SHARED @@ -401,6 +408,7 @@ add_library(livekit SHARED src/token_source_json.cpp src/token_source_jwt.cpp src/token_source_internal.h + src/uniffi_bindgen_adapter.cpp src/local_participant.cpp src/remote_participant.cpp src/stats.cpp @@ -445,7 +453,14 @@ if(UNIX AND NOT APPLE) endif() -target_sources(livekit PRIVATE $) +target_sources(livekit PRIVATE + $ + $ +) +set_source_files_properties( + ${LIVEKIT_ROOT_DIR}/src/uniffi_bindgen_adapter.cpp + PROPERTIES OBJECT_DEPENDS ${LIVEKIT_UNIFFI_CPP_HEADER} +) target_include_directories(livekit PUBLIC @@ -454,6 +469,7 @@ target_include_directories(livekit PRIVATE ${LIVEKIT_ROOT_DIR}/src ${LIVEKIT_ROOT_DIR}/src/trace + ${LIVEKIT_UNIFFI_CPP_GENERATED_DIR} ${LIVEKIT_PROTOBUF_DEP_INCLUDE_DIRS} ) diff --git a/build.cmd b/build.cmd index 4869ebe7..e91d2483 100644 --- a/build.cmd +++ b/build.cmd @@ -336,15 +336,10 @@ if exist "%LOCAL_INSTALL_DIR%" ( rmdir /s /q "%LOCAL_INSTALL_DIR%" 2>nul ) -echo Removing Rust debug artifacts... -if exist "%PROJECT_ROOT%\client-sdk-rust\target\debug" ( - rmdir /s /q "%PROJECT_ROOT%\client-sdk-rust\target\debug" 2>nul -) - -echo Removing Rust release artifacts... -if exist "%PROJECT_ROOT%\client-sdk-rust\target\release" ( - rmdir /s /q "%PROJECT_ROOT%\client-sdk-rust\target\release" 2>nul -) +echo Removing Rust target directory... +if exist "%PROJECT_ROOT%\client-sdk-rust\target" ( + rmdir /s /q "%PROJECT_ROOT%\client-sdk-rust\target" 2>nul +) echo ==^> Clean-all complete. goto :eof diff --git a/build.sh b/build.sh index c3257394..48cb27f1 100755 --- a/build.sh +++ b/build.sh @@ -347,11 +347,8 @@ clean_all() { echo "Removing local-install directory..." rm -rf "${LOCAL_INSTALL_DIR}" || true - echo "Removing Rust debug artifacts..." - rm -rf "${PROJECT_ROOT}/client-sdk-rust/target/debug" || true - - echo "Removing Rust release artifacts..." - rm -rf "${PROJECT_ROOT}/client-sdk-rust/target/release" || true + echo "Removing Rust target directory..." + rm -rf "${PROJECT_ROOT}/client-sdk-rust/target" || true echo "==> Clean-all complete." } diff --git a/client-sdk-rust b/client-sdk-rust index bae4df2f..eb54f6bc 160000 --- a/client-sdk-rust +++ b/client-sdk-rust @@ -1 +1 @@ -Subproject commit bae4df2f3d6c8f0f6ce81a7370c445779f1948d2 +Subproject commit eb54f6bce0cf4c9544ed2b9a14fc31787342118c diff --git a/cmake/uniffi_cpp.cmake b/cmake/uniffi_cpp.cmake new file mode 100644 index 00000000..1061bd05 --- /dev/null +++ b/cmake/uniffi_cpp.cmake @@ -0,0 +1,127 @@ +# 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. + +set(LIVEKIT_UNIFFI_CPP_GENERATED_DIR "${LIVEKIT_BINARY_DIR}/generated/uniffi") +set(LIVEKIT_UNIFFI_CPP_SOURCE + "${LIVEKIT_UNIFFI_CPP_GENERATED_DIR}/livekit_ffi.cpp") +set(LIVEKIT_UNIFFI_CPP_HEADER + "${LIVEKIT_UNIFFI_CPP_GENERATED_DIR}/livekit_ffi.hpp") +set(LIVEKIT_UNIFFI_CPP_SCAFFOLDING_HEADER + "${LIVEKIT_UNIFFI_CPP_GENERATED_DIR}/livekit_ffi_scaffolding.hpp") + +# Release Linux builds strip the UniFFI metadata symbols, so generate from a +# separate unstripped Debug library there. PE and Mach-O retain the metadata, +# allowing Windows and macOS to reuse the livekit-ffi library already built for +# the selected CMake configuration. +if(UNIX AND NOT APPLE) + set(LIVEKIT_UNIFFI_METADATA_TARGET_DIR + "${RUST_ROOT}/target/uniffi-cpp-metadata") + set(LIVEKIT_UNIFFI_METADATA_LIBRARY + "${LIVEKIT_UNIFFI_METADATA_TARGET_DIR}/debug/liblivekit_ffi.so") +endif() + +file(GLOB_RECURSE LIVEKIT_UNIFFI_RUST_SOURCES CONFIGURE_DEPENDS + "${RUST_ROOT}/livekit-ffi/src/*.rs" + "${RUST_ROOT}/livekit-ffi/Cargo.toml" +) +list(APPEND LIVEKIT_UNIFFI_RUST_SOURCES + "${RUST_ROOT}/Cargo.toml" + "${RUST_ROOT}/Cargo.lock" + "${RUST_ROOT}/rust-toolchain.toml" +) + +file(GLOB_RECURSE LIVEKIT_UNIFFI_BINDGEN_SOURCES CONFIGURE_DEPENDS + "${RUST_ROOT}/tools/bindgens/src/*.rs" + "${RUST_ROOT}/tools/bindgens/Cargo.toml" +) + +if(UNIX AND NOT APPLE) + add_custom_command( + OUTPUT "${LIVEKIT_UNIFFI_METADATA_LIBRARY}" + COMMAND "${CMAKE_COMMAND}" + -DCFG=Debug + -DRUST_ROOT=${RUST_ROOT} + -DCARGO=${CARGO_EXECUTABLE} + -DPROTOC_PATH=${Protobuf_PROTOC_EXECUTABLE} + -DGCC_LIB_DIR=${GCC_LIB_DIR} + -DCARGO_TARGET_DIR=${LIVEKIT_UNIFFI_METADATA_TARGET_DIR} + -P "${RUN_CARGO_SCRIPT}" + WORKING_DIRECTORY "${RUST_ROOT}" + DEPENDS ${LIVEKIT_UNIFFI_RUST_SOURCES} + COMMENT "Building unstripped livekit-ffi metadata library" + VERBATIM + ) + add_custom_target(build_livekit_ffi_metadata + DEPENDS "${LIVEKIT_UNIFFI_METADATA_LIBRARY}") +else() + set(LIVEKIT_UNIFFI_METADATA_LIBRARY "$") + add_custom_target(build_livekit_ffi_metadata) +endif() + +# Keep Cargo invocations serialized on fresh runners, and ensure the selected +# livekit-ffi library exists before it is inspected on Windows and macOS. +add_dependencies(build_livekit_ffi_metadata build_rust_ffi) + +add_custom_command( + OUTPUT + "${LIVEKIT_UNIFFI_CPP_SOURCE}" + "${LIVEKIT_UNIFFI_CPP_HEADER}" + "${LIVEKIT_UNIFFI_CPP_SCAFFOLDING_HEADER}" + COMMAND "${CMAKE_COMMAND}" -E make_directory + "${LIVEKIT_UNIFFI_CPP_GENERATED_DIR}" + COMMAND "${CMAKE_COMMAND}" -E env + "CARGO_TARGET_DIR=${RUST_ROOT}/target/bindgens" + "CARGO_ENCODED_RUSTFLAGS=" + "RUSTFLAGS=" + "${CARGO_EXECUTABLE}" run --locked + --package bindgens + --bin uniffi-bindgen-cpp + -- + --library "${LIVEKIT_UNIFFI_METADATA_LIBRARY}" + --out-dir "${LIVEKIT_UNIFFI_CPP_GENERATED_DIR}" + WORKING_DIRECTORY "${RUST_ROOT}" + DEPENDS + build_livekit_ffi_metadata + ${LIVEKIT_UNIFFI_RUST_SOURCES} + ${LIVEKIT_UNIFFI_BINDGEN_SOURCES} + COMMENT "Generating UniFFI C++ bindings" + VERBATIM +) +add_custom_target(generate_livekit_ffi_uniffi_cpp + DEPENDS + "${LIVEKIT_UNIFFI_CPP_SOURCE}" + "${LIVEKIT_UNIFFI_CPP_HEADER}" + "${LIVEKIT_UNIFFI_CPP_SCAFFOLDING_HEADER}" +) + +set_source_files_properties( + "${LIVEKIT_UNIFFI_CPP_SOURCE}" + "${LIVEKIT_UNIFFI_CPP_HEADER}" + "${LIVEKIT_UNIFFI_CPP_SCAFFOLDING_HEADER}" + PROPERTIES GENERATED TRUE +) + +add_library(livekit_uniffi_cpp OBJECT + "${LIVEKIT_UNIFFI_CPP_SOURCE}" +) +set_target_properties(livekit_uniffi_cpp PROPERTIES + CXX_VISIBILITY_PRESET hidden + C_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON +) +add_dependencies(livekit_uniffi_cpp generate_livekit_ffi_uniffi_cpp) +target_include_directories(livekit_uniffi_cpp + PUBLIC "${LIVEKIT_UNIFFI_CPP_GENERATED_DIR}" +) +target_link_libraries(livekit_uniffi_cpp PRIVATE livekit_ffi) diff --git a/src/tests/unit/test_uniffi.cpp b/src/tests/unit/test_uniffi.cpp new file mode 100644 index 00000000..50c4adfb --- /dev/null +++ b/src/tests/unit/test_uniffi.cpp @@ -0,0 +1,34 @@ +/* + * 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. + */ + +#include + +#include "uniffi_bindgen_adapter.h" + +namespace livekit { +namespace { + +TEST(UniFfiTest, BuildVersion) { + const auto version = uniffiBindgenBuildVersion(); + + ASSERT_TRUE(version.has_value()) << "Generated UniFFI binding did not return a build version"; + + EXPECT_FALSE(version.value().empty()); + EXPECT_NE(version.value(), "unknown"); +} + +} // namespace +} // namespace livekit diff --git a/src/uniffi_bindgen_adapter.cpp b/src/uniffi_bindgen_adapter.cpp new file mode 100644 index 00000000..bcdc1b9a --- /dev/null +++ b/src/uniffi_bindgen_adapter.cpp @@ -0,0 +1,31 @@ +/* + * 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. + */ + +#include "uniffi_bindgen_adapter.h" + +#include "livekit_ffi.hpp" + +namespace livekit { + +std::optional uniffiBindgenBuildVersion() { + auto version = livekit_ffi::build_version(); + if (version.empty() || version == "unknown") { + return std::nullopt; + } + return version; +} + +} // namespace livekit diff --git a/src/uniffi_bindgen_adapter.h b/src/uniffi_bindgen_adapter.h new file mode 100644 index 00000000..d3a56c94 --- /dev/null +++ b/src/uniffi_bindgen_adapter.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +/// +/// Note: This adapter is a temporary internal translation unit for exercising UniFFI +/// bindings. It will be removed when the represented UniFFI APIs are migrated to actual +/// SDK features. +/// + +#include "livekit/visibility.h" + +namespace livekit { + +/// @brief Gets the build version through the generated UniFFI binding. +/// +/// @return The generated binding's build version, or no value if it is invalid +/// or the binding call fails. +LIVEKIT_INTERNAL_API std::optional uniffiBindgenBuildVersion(); + +} // namespace livekit