diff --git a/AGENTS.md b/AGENTS.md index 366d95af..cf850d48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -240,11 +240,37 @@ Adhere to clang-tidy checks configured in `.clang-tidy`. Run `./scripts/clang-ti | Dependency | Scope | Notes | |------------|-------|-------| -| protobuf | Private (built-in) | Vendored via FetchContent (Unix) or vcpkg (Windows) | +| protobuf-lite | Private (built-in) | Vendored via FetchContent (Unix) or vcpkg (Windows). The SDK links **only** against `protobuf::libprotobuf-lite`; the generated FFI code uses `MessageLite`. See "Protobuf runtime" below. | | spdlog | **Private** | FetchContent or system package; must NOT leak into public API | | client-sdk-rust | Build-time | Git submodule, built via cargo during CMake build | | Google Test | Test only | FetchContent in `src/tests/CMakeLists.txt` | +### Protobuf runtime + +The C++ SDK uses the **protobuf-lite** runtime, not the full protobuf runtime. +The FFI `.proto` files in `client-sdk-rust/livekit-ffi/protocol/` do not opt +into lite themselves (they are shared with the Rust and Node FFI generators). +Instead, the C++ build copies them into the build tree and appends +`option optimize_for = LITE_RUNTIME;` before running `protoc`. See +`cmake/patch_protos_for_lite.cmake` and the `PATCHED_PROTO_FILES` custom +command in the top-level `CMakeLists.txt`. The original `.proto` files in the +submodule are never modified. + +Consequences for code in `src/`: + +- The generated `proto::*` classes extend `google::protobuf::MessageLite`, not + `Message`. Only the lite subset of the protobuf API is available. +- Allowed: `set_*` / `mutable_*` / `add_*` / `has_*` / `clear_*` / `*_size()` / + oneof `_case()` accessors, `SerializeToString`, `ParseFromArray`, `CopyFrom`, + assignment, `google::protobuf::RepeatedField`/`RepeatedPtrField`. +- **Not available**: `DebugString` / `ShortDebugString` / `Utf8DebugString`, + `TextFormat`, `JsonStringToMessage` / `MessageToJsonString`, `GetReflection`, + `GetDescriptor`, `DescriptorPool`, `MessageFactory`, Well-Known Types + (`Any` / `Timestamp` / `Duration` / `Struct` / `FieldMask` / `Empty`), + reflection-based copy/merge utilities. Do not introduce code that needs them. +- If you ever need a human-readable dump of a proto message for logging, build + a hand-written formatter — do not reach for `DebugString()`. + When adding a new private/vendored dependency to this table, also add a forbidden symbol pattern for it to `.github/scripts/check_no_private_symbols.py` so the "Symbol leak check" diff --git a/CMakeLists.txt b/CMakeLists.txt index 895284b1..93c21360 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,24 +70,55 @@ if(LIVEKIT_IS_TOPLEVEL) endif() set(FFI_PROTO_DIR ${LIVEKIT_ROOT_DIR}/client-sdk-rust/livekit-ffi/protocol) -set(FFI_PROTO_FILES - ${FFI_PROTO_DIR}/handle.proto - ${FFI_PROTO_DIR}/ffi.proto - ${FFI_PROTO_DIR}/participant.proto - ${FFI_PROTO_DIR}/room.proto - ${FFI_PROTO_DIR}/track.proto - ${FFI_PROTO_DIR}/video_frame.proto - ${FFI_PROTO_DIR}/audio_frame.proto - ${FFI_PROTO_DIR}/e2ee.proto - ${FFI_PROTO_DIR}/stats.proto - ${FFI_PROTO_DIR}/data_stream.proto - ${FFI_PROTO_DIR}/data_track.proto - ${FFI_PROTO_DIR}/rpc.proto - ${FFI_PROTO_DIR}/track_publication.proto +set(FFI_PROTO_NAMES + handle.proto + ffi.proto + participant.proto + room.proto + track.proto + video_frame.proto + audio_frame.proto + e2ee.proto + stats.proto + data_stream.proto + data_track.proto + rpc.proto + track_publication.proto ) +set(FFI_PROTO_FILES) +foreach(_p ${FFI_PROTO_NAMES}) + list(APPEND FFI_PROTO_FILES "${FFI_PROTO_DIR}/${_p}") +endforeach() set(PROTO_BINARY_DIR ${LIVEKIT_BINARY_DIR}/generated) file(MAKE_DIRECTORY ${PROTO_BINARY_DIR}) +# Staging area for protobuf-lite-patched copies of the FFI .proto files. +# We never modify the originals (they're shared with the Rust submodule and +# other language wrappers); instead we copy them here and append +# `option optimize_for = LITE_RUNTIME;` so protoc emits MessageLite-based code +# that links only against libprotobuf-lite. +set(PROTO_LITE_SRC_DIR ${PROTO_BINARY_DIR}/proto-lite) +file(MAKE_DIRECTORY ${PROTO_LITE_SRC_DIR}) + +set(PATCHED_PROTO_FILES) +foreach(_p ${FFI_PROTO_NAMES}) + list(APPEND PATCHED_PROTO_FILES "${PROTO_LITE_SRC_DIR}/${_p}") +endforeach() + +add_custom_command( + OUTPUT ${PATCHED_PROTO_FILES} + COMMAND ${CMAKE_COMMAND} + -DIN_DIR=${FFI_PROTO_DIR} + -DOUT_DIR=${PROTO_LITE_SRC_DIR} + "-DFILES=${FFI_PROTO_NAMES}" + -P ${LIVEKIT_ROOT_DIR}/cmake/patch_protos_for_lite.cmake + DEPENDS + ${FFI_PROTO_FILES} + ${LIVEKIT_ROOT_DIR}/cmake/patch_protos_for_lite.cmake + COMMENT "Patching .proto files with optimize_for = LITE_RUNTIME (C++ build only)" + VERBATIM +) + # Livekit static protobuf. include(protobuf) # spdlog logging library (PRIVATE dependency). @@ -100,7 +131,7 @@ elseif(NOT Protobuf_PROTOC_EXECUTABLE) endif() message(STATUS "Using protoc: ${Protobuf_PROTOC_EXECUTABLE}") -add_library(livekit_proto OBJECT ${FFI_PROTO_FILES}) +add_library(livekit_proto OBJECT ${PATCHED_PROTO_FILES}) # livekit_proto is compiled into liblivekit; apply the same hidden visibility # policy so generated protobuf code does not leak into the SDK's exported ABI. set_target_properties(livekit_proto PROPERTIES @@ -108,10 +139,16 @@ set_target_properties(livekit_proto PROPERTIES C_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON ) -if(TARGET protobuf::libprotobuf) - set(LIVEKIT_PROTOBUF_TARGET protobuf::libprotobuf) +# The C++ build links against protobuf-lite only. Generated code uses +# MessageLite (no Reflection/Descriptor/TextFormat/JSON), and the SDK does +# not use any of those APIs. See cmake/patch_protos_for_lite.cmake. +if(TARGET protobuf::libprotobuf-lite) + set(LIVEKIT_PROTOBUF_TARGET protobuf::libprotobuf-lite) else() - message(FATAL_ERROR "No protobuf library target found (expected protobuf::libprotobuf)") + message(FATAL_ERROR + "protobuf::libprotobuf-lite target not found. The LiveKit C++ SDK requires " + "protobuf-lite. On Linux/macOS this comes from the vendored protobuf build; " + "on Windows it comes from the vcpkg protobuf port.") endif() target_include_directories(livekit_proto PRIVATE "${PROTO_BINARY_DIR}" @@ -125,11 +162,13 @@ if(TARGET absl::base) endif() endif() -# Manually generate protobuf files to avoid path prefix issues +# Manually generate protobuf files to avoid path prefix issues. The inputs are +# the patched copies in ${PROTO_LITE_SRC_DIR}, not the originals — see the +# PATCHED_PROTO_FILES custom command above. set(PROTO_SRCS) set(PROTO_HDRS) -foreach(PROTO_FILE ${FFI_PROTO_FILES}) - get_filename_component(PROTO_NAME ${PROTO_FILE} NAME_WE) +foreach(_name ${FFI_PROTO_NAMES}) + get_filename_component(PROTO_NAME "${_name}" NAME_WE) list(APPEND PROTO_SRCS "${PROTO_BINARY_DIR}/${PROTO_NAME}.pb.cc") list(APPEND PROTO_HDRS "${PROTO_BINARY_DIR}/${PROTO_NAME}.pb.h") endforeach() @@ -137,11 +176,11 @@ endforeach() add_custom_command( OUTPUT ${PROTO_SRCS} ${PROTO_HDRS} COMMAND ${Protobuf_PROTOC_EXECUTABLE} - --proto_path=${FFI_PROTO_DIR} + --proto_path=${PROTO_LITE_SRC_DIR} --cpp_out=${PROTO_BINARY_DIR} - ${FFI_PROTO_FILES} - DEPENDS ${FFI_PROTO_FILES} - COMMENT "Generating C++ protobuf files" + ${PATCHED_PROTO_FILES} + DEPENDS ${PATCHED_PROTO_FILES} + COMMENT "Generating C++ protobuf files (lite runtime)" VERBATIM ) diff --git a/cmake/patch_protos_for_lite.cmake b/cmake/patch_protos_for_lite.cmake new file mode 100644 index 00000000..d981f0e9 --- /dev/null +++ b/cmake/patch_protos_for_lite.cmake @@ -0,0 +1,55 @@ +# cmake/patch_protos_for_lite.cmake +# +# Copy upstream .proto files (from client-sdk-rust/livekit-ffi/protocol/) into +# a build-tree staging directory and append `option optimize_for = LITE_RUNTIME;` +# to each one. The C++ SDK then compiles the patched copies, which causes +# protoc to emit code linking only against libprotobuf-lite (no reflection, +# no descriptors, no text-format, no JSON). +# +# This patches *only* the copies used by the C++ build. The original .proto +# files in the client-sdk-rust submodule are untouched, so prost (Rust) and +# protobuf-es (Node) generators continue to see the unmodified sources. +# +# Usage (invoked via `cmake -P`): +# cmake -DIN_DIR= -DOUT_DIR= "-DFILES=a.proto;b.proto;..." \ +# -P patch_protos_for_lite.cmake + +if(NOT DEFINED IN_DIR OR IN_DIR STREQUAL "") + message(FATAL_ERROR "patch_protos_for_lite.cmake: IN_DIR is required") +endif() +if(NOT DEFINED OUT_DIR OR OUT_DIR STREQUAL "") + message(FATAL_ERROR "patch_protos_for_lite.cmake: OUT_DIR is required") +endif() +if(NOT DEFINED FILES OR FILES STREQUAL "") + message(FATAL_ERROR "patch_protos_for_lite.cmake: FILES is required") +endif() + +file(MAKE_DIRECTORY "${OUT_DIR}") + +set(_marker "// --- appended by client-sdk-cpp: force lite runtime for C++ codegen ---") +set(_option_line "option optimize_for = LITE_RUNTIME;") + +foreach(_name IN LISTS FILES) + set(_src "${IN_DIR}/${_name}") + set(_dst "${OUT_DIR}/${_name}") + + if(NOT EXISTS "${_src}") + message(FATAL_ERROR "patch_protos_for_lite.cmake: missing source ${_src}") + endif() + + file(READ "${_src}" _content) + + # Defensive: refuse to patch if an explicit optimize_for is already present; + # honor whatever upstream chose rather than silently overriding it. + string(REGEX MATCH "option[ \t]+optimize_for" _existing "${_content}") + if(_existing) + message(FATAL_ERROR + "patch_protos_for_lite.cmake: ${_name} already declares optimize_for. " + "Remove the LITE_RUNTIME patch or upstream the option instead.") + endif() + + # Append the option as a trailing line. File-level options have no ordering + # requirement in protobuf grammar, so this is always safe. + file(WRITE "${_dst}" + "${_content}\n${_marker}\n${_option_line}\n") +endforeach() diff --git a/cmake/protobuf.cmake b/cmake/protobuf.cmake index 8132a084..9ce80c7a 100644 --- a/cmake/protobuf.cmake +++ b/cmake/protobuf.cmake @@ -183,13 +183,18 @@ else() message(FATAL_ERROR "Vendored protobuf did not create a protoc target") endif() -# Prefer protobuf-lite (optional; keep libprotobuf around too) +# protobuf-lite is REQUIRED. The LiveKit C++ SDK builds its generated FFI +# protobuf code with `option optimize_for = LITE_RUNTIME` (see +# cmake/patch_protos_for_lite.cmake) and links only against libprotobuf-lite. if(TARGET protobuf::libprotobuf-lite) # ok elseif(TARGET libprotobuf-lite) add_library(protobuf::libprotobuf-lite ALIAS libprotobuf-lite) else() - message(WARNING "Vendored protobuf did not create protobuf-lite target; continuing with libprotobuf only") + message(FATAL_ERROR + "Vendored protobuf did not create a libprotobuf-lite target. " + "The LiveKit C++ SDK requires protobuf-lite (LITE_RUNTIME). " + "Check LIVEKIT_PROTOBUF_VERSION='${LIVEKIT_PROTOBUF_VERSION}'.") endif() # Include dirs: prefer target usage; keep this var for your existing CMakeLists.