Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/scripts/check_no_private_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -53,6 +57,10 @@
"google::protobuf",
"absl::",
"nlohmann::",
# Generated UniFFI C++ binding API and runtime implementation.
"livekit_ffi::",
"uniffi::",
"uniffi_",
]

MAX_REPORTED_LEAKS = 20
Expand Down Expand Up @@ -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"
)
Expand Down
16 changes: 11 additions & 5 deletions .github/workflows/cpp-tools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,23 @@ 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
sudo chmod a+r /etc/apt/keyrings/llvm.asc
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
Expand All @@ -104,15 +106,19 @@ 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

- 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 }}
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
18 changes: 17 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}\")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
stephen-derosa marked this conversation as resolved.
src/local_participant.cpp
src/remote_participant.cpp
src/stats.cpp
Expand Down Expand Up @@ -445,7 +453,14 @@ if(UNIX AND NOT APPLE)
endif()


target_sources(livekit PRIVATE $<TARGET_OBJECTS:livekit_proto>)
target_sources(livekit PRIVATE
$<TARGET_OBJECTS:livekit_proto>
$<TARGET_OBJECTS:livekit_uniffi_cpp>
)
set_source_files_properties(
Comment thread
alan-george-lk marked this conversation as resolved.
${LIVEKIT_ROOT_DIR}/src/uniffi_bindgen_adapter.cpp
PROPERTIES OBJECT_DEPENDS ${LIVEKIT_UNIFFI_CPP_HEADER}
)

target_include_directories(livekit
PUBLIC
Expand All @@ -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}
)

Expand Down
13 changes: 4 additions & 9 deletions build.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 2 additions & 5 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Expand Down
2 changes: 1 addition & 1 deletion client-sdk-rust
127 changes: 127 additions & 0 deletions cmake/uniffi_cpp.cmake
Comment thread
alan-george-lk marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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 "$<TARGET_FILE:livekit_ffi>")
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="
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
"${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}"
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
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)
34 changes: 34 additions & 0 deletions src/tests/unit/test_uniffi.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>

#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
31 changes: 31 additions & 0 deletions src/uniffi_bindgen_adapter.cpp
Original file line number Diff line number Diff line change
@@ -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<std::string> uniffiBindgenBuildVersion() {
auto version = livekit_ffi::build_version();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is livekit_ffi::build_version() a rust impl thats getting called in cpp?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i.e. is this the "Secret Sauce"

@alan-george-lk alan-george-lk Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is currently the only-available uniffi exported function in livekit-ffi: https://github.com/livekit/rust-sdks/blob/main/livekit-ffi/src/build_info.rs#L20-L23

This is the secret sauce in the sense that livekit_ffi is the uniffi generated namespace, and build_version is rust impl interfaced by the bindgen, so yes.

I tested all of this infra against livekit-uniffi but that is not actually targeted for critical path (per @1egoman), but allowed me to test more things:

https://github.com/livekit/rust-sdks/blob/main/livekit-uniffi/src/access_token.rs#L185-L189

and

https://github.com/livekit/rust-sdks/blob/main/livekit-uniffi/src/log_forward.rs#L27-L32

(removed from this branch for PR but part of history)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

sweet thanks!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

shouldn't we expoe the ffi version via the public livekit header instead ?

livekit_ffi.hpp seems internal to me.

if (version.empty() || version == "unknown") {
return std::nullopt;
}
return version;
}

} // namespace livekit
Loading
Loading