From 13b26187040930cf67e7a1f8c50231c3399afcdf Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 21:39:14 -0700 Subject: [PATCH 01/25] quest: claim quest/m1/c/package From 1511e0ee3bd26e0b5bb9f76f2a9261c180de011d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:14:14 -0700 Subject: [PATCH 02/25] feat(c)!: ship the generated C API as the moq-c package c/moq renders moq-ffi through uniffi-bindgen-cpp's C backend, compiles the C header's C++ implementation with a hand-written moq_shutdown, and installs moq-c 0.8.0: libmoq plus moq-ffi's staticlib, , a CMake config exporting moq::c, and moq-c.pc, both honoring CMAKE_INSTALL_LIBDIR. A C-only probe links the installed package through find_package and pkg-config over a loopback QUIC session, with callbacks on its own loop. release-c.yml, the nightly, platform.yml, and a clang job mirror the C++ package. MoqAudioEncoderFinish becomes MoqAudioEncoderFinished so its C name no longer collides with MoqAudioEncoder::finish. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/alert.yml | 1 + .github/workflows/c.yml | 56 +++ .github/workflows/nightly.yml | 10 + .github/workflows/platform.yml | 10 +- .github/workflows/release-c.yml | 169 ++++++++ AGENTS.md | 4 +- c/justfile | 28 ++ c/moq/.gitignore | 2 + c/moq/CMakeLists.txt | 215 +++++++++++ c/moq/MIGRATE.md | 199 ++++++++++ c/moq/README.md | 23 ++ c/moq/VERSION | 1 + c/moq/build.sh | 93 +++++ c/moq/cmake/moq-c-config.cmake.in | 20 + c/moq/cmake/moq-c.pc.in | 9 + c/moq/include/moq/moq.h | 21 + c/moq/src/shutdown.c | 10 + c/moq/test/CMakeLists.txt | 19 + c/moq/test/probe.c | 365 ++++++++++++++++++ cpp/moq/MIGRATE.md | 4 + dart/moq/lib/audio.dart | 2 +- dart/moq_ffi/lib/src/moq.dart | 31 +- doc/index.md | 2 +- doc/lib/c/index.md | 10 + doc/lib/cpp/index.md | 3 +- flake.nix | 13 +- go/wrapper/audio/audio.go | 2 +- justfile | 3 +- .../kotlin/dev/moq/audio/Audio.kt | 2 +- py/moq-rs/moq/audio.py | 4 +- quest/m1/c/README.md | 1 - quest/m1/c/consumers.md | 4 - quest/m1/c/package.md | 33 -- rs/moq-ffi/MIGRATE.md | 5 + rs/moq-ffi/src/audio.rs | 6 +- sh/c/check.sh | 82 ++++ sh/dispatch-query.test.py | 2 +- sh/dispatch.sh | 11 +- swift/Sources/Moq/Audio.swift | 2 +- 39 files changed, 1398 insertions(+), 79 deletions(-) create mode 100644 .github/workflows/c.yml create mode 100644 .github/workflows/release-c.yml create mode 100644 c/justfile create mode 100644 c/moq/.gitignore create mode 100644 c/moq/CMakeLists.txt create mode 100644 c/moq/MIGRATE.md create mode 100644 c/moq/README.md create mode 100644 c/moq/VERSION create mode 100755 c/moq/build.sh create mode 100644 c/moq/cmake/moq-c-config.cmake.in create mode 100644 c/moq/cmake/moq-c.pc.in create mode 100644 c/moq/include/moq/moq.h create mode 100644 c/moq/src/shutdown.c create mode 100644 c/moq/test/CMakeLists.txt create mode 100644 c/moq/test/probe.c delete mode 100644 quest/m1/c/package.md create mode 100755 sh/c/check.sh diff --git a/.github/workflows/alert.yml b/.github/workflows/alert.yml index acf165f40da..59a797edaaa 100644 --- a/.github/workflows/alert.yml +++ b/.github/workflows/alert.yml @@ -23,6 +23,7 @@ on: - moq.sh - Nightly - release-brew + - Release C - Release Cpp - Release Dart - Release Dart FFI diff --git a/.github/workflows/c.yml b/.github/workflows/c.yml new file mode 100644 index 00000000000..b01fd717951 --- /dev/null +++ b/.github/workflows/c.yml @@ -0,0 +1,56 @@ +name: C + +# Builds and tests the moq C package (c/moq) with clang, outside Nix. +# platform.yml covers MSVC. `just check` already runs `just c check` with the +# dev shell's gcc; the dev shell has no clang, and adding one would change which +# compiler every other recipe gets as `cc`. +# +# Outside Nix the generator comes from `cargo install` at the tag flake.nix +# pins; see the comment there for every other place that names it. + +permissions: + contents: read + +on: + pull_request: + # `closed` is here only so merging/closing a PR cancels its in-flight run + # via the concurrency group below; the job itself is skipped on close. + types: [opened, synchronize, reopened, closed] + paths: + - "c/**" + - "rs/moq-ffi/**" + - "sh/c/**" + - "rs/moq-c/native-libs/**" + - "Cargo.lock" + - ".github/workflows/c.yml" + +concurrency: + group: c-${{ github.ref }} + cancel-in-progress: true + +jobs: + clang: + name: C (clang) + if: github.event.action != 'closed' + runs-on: ubuntu-24.04 + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + + - uses: ./.github/actions/just + + - name: Install uniffi-bindgen-cpp + run: cargo install uniffi-bindgen-cpp --locked --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.5+v0.32.2 + + - name: Check + run: just c check + env: + CC: clang + CXX: clang++ diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 39f0f083cae..5ca61991e2e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -486,6 +486,16 @@ jobs: permissions: contents: write + release-c: + name: Release C + needs: [release-shared-ffi] + if: ${{ always() && !cancelled() }} + uses: ./.github/workflows/release-c.yml + with: + reuse-native: true + permissions: + contents: write + release-moq-relay: name: moq-relay uses: ./.github/workflows/moq-relay.yml diff --git a/.github/workflows/platform.yml b/.github/workflows/platform.yml index 81192d44783..b4023a7317d 100644 --- a/.github/workflows/platform.yml +++ b/.github/workflows/platform.yml @@ -11,6 +11,7 @@ on: branches: [release] paths: - "rs/**" + - "c/**" - "cpp/**" - "swift/**" - "infra/moq-sh/**" @@ -64,6 +65,7 @@ jobs: needed: ${{ steps.scope.outputs.needed }} rs: ${{ steps.scope.outputs.rs }} swift: ${{ steps.scope.outputs.swift }} + c: ${{ steps.scope.outputs.c }} cpp: ${{ steps.scope.outputs.cpp }} obs: ${{ steps.scope.outputs.obs_compile }} moq_sh: ${{ steps.scope.outputs.moq_sh }} @@ -82,7 +84,7 @@ jobs: windows: name: Windows needs: scope - if: needs.scope.outputs.rs == 'true' || needs.scope.outputs.cpp == 'true' || needs.scope.outputs.obs == 'true' + if: needs.scope.outputs.rs == 'true' || needs.scope.outputs.c == 'true' || needs.scope.outputs.cpp == 'true' || needs.scope.outputs.obs == 'true' # OBS uses the Visual Studio 17 2022 CMake generator. runs-on: windows-2022 # The suites run serially without a Rust cache; the full macOS suite measured 43 min. @@ -106,7 +108,7 @@ jobs: with: tool: nextest@0.9.146 - name: Install uniffi-bindgen-cpp - if: needs.scope.outputs.cpp == 'true' || needs.scope.outputs.obs == 'true' + if: needs.scope.outputs.c == 'true' || needs.scope.outputs.cpp == 'true' || needs.scope.outputs.obs == 'true' shell: bash run: cargo install uniffi-bindgen-cpp --locked --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.5+v0.32.2 - name: Rust compile and tests @@ -117,6 +119,10 @@ jobs: run: | just rs windows "$CI_BASE" just rs windows-test + - name: C + if: needs.scope.outputs.c == 'true' + shell: bash + run: just c check - name: C++ if: needs.scope.outputs.cpp == 'true' shell: bash diff --git a/.github/workflows/release-c.yml b/.github/workflows/release-c.yml new file mode 100644 index 00000000000..a16314ab5ba --- /dev/null +++ b/.github/workflows/release-c.yml @@ -0,0 +1,169 @@ +name: Release C + +# Builds the moq C package (c/moq) for every target and publishes one archive per target to a +# c-v GitHub release. moq-c-v* tags belong to the hand-written rs/moq-c crate until it +# retires. + +on: + push: + tags: + - "c-v*" + # Dry-run: build everything, publish nothing. Nightly calls every release + # workflow this way so a broken build surfaces before the tag is cut. + workflow_call: + inputs: + reuse-native: + description: "Use this run's native input after its installed-consumer gate succeeds" + type: boolean + default: false + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-24.04-arm + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: ./.github/actions/just + + - name: Install Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + + # aws-lc-rs assembles its x86_64 crypto with NASM on Windows. + - name: Install NASM + if: runner.os == 'Windows' + shell: pwsh + run: | + choco install nasm -y --no-progress + "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + # The tag flake.nix pins; see the comment there. + - name: Install uniffi-bindgen-cpp + shell: bash + run: cargo install uniffi-bindgen-cpp --locked --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.5+v0.32.2 + + # The package version lives in c/moq/VERSION; a tag that disagrees is a mistake. + - name: Check version + if: github.ref_type == 'tag' + shell: bash + env: + TAG: ${{ github.ref_name }} + run: | + version=$(tr -d '[:space:]' < c/moq/VERSION) + if [[ "$TAG" != "c-v$version" ]]; then + echo "::error::tag $TAG does not match c/moq/VERSION ($version)" + exit 1 + fi + + - name: Refuse same-run input for a release tag + if: inputs.reuse-native && github.ref_type == 'tag' + run: | + echo "::error::c-v releases must use their immutable FFI_VERSION pin" + exit 1 + + # release-shared-ffi.yml stages and checks one default-feature input for both packages. + - name: Download validated same-run input + if: inputs.reuse-native + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 + with: + name: cpp-native-${{ matrix.target }} + path: native-archive + + - name: Parse prebuilt FFI version + if: inputs.reuse-native + id: ffi-version + run: just gh release parse-version moq-ffi + + - name: Verify and extract validated input + if: inputs.reuse-native + shell: bash + env: + TARGET: ${{ matrix.target }} + VERSION: ${{ steps.ffi-version.outputs.version }} + run: just cpp ffi extract --version "$VERSION" --target "$TARGET" --directory native-archive --output native-input + + - name: Build and package + shell: bash + env: + TARGET: ${{ matrix.target }} + PREBUILT_INPUT: ${{ inputs.reuse-native }} + run: | + args=(--target "$TARGET" --output dist) + if [[ "$PREBUILT_INPUT" == true ]]; then + args+=(--ffi-dir "$GITHUB_WORKSPACE/native-input") + fi + just c package "${args[@]}" + + - name: Check packaged archive + shell: bash + env: + TARGET: ${{ matrix.target }} + run: | + version=$(tr -d '[:space:]' < c/moq/VERSION) + extension=tar.gz + [[ "$TARGET" == *-windows-* ]] && extension=zip + just c check --archive "dist/moq-c-$version-$TARGET.$extension" + + - name: Upload artifact + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: moq-c-generated-${{ matrix.target }} + path: dist/* + + release: + name: Release + needs: build + if: github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: ./.github/actions/just + + - name: Parse version + id: parse + run: just gh release parse-version c + + - name: Find previous tag + id: prev_tag + run: just gh release prev-tag c + + - name: Download artifacts + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 + with: + path: artifacts + merge-multiple: true + + - name: Create or update release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + RELEASE_TITLE: "moq-c v${{ steps.parse.outputs.version }}" + RELEASE_PREV_TAG: ${{ steps.prev_tag.outputs.tag }} + run: just gh release create artifacts diff --git a/AGENTS.md b/AGENTS.md index ae95b6197f2..bac2807e89f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,7 +106,7 @@ The `quest` binary comes from the kixelated/quest flake input and serves the que | Change in | Also update | |---|---| -| `rs/moq-ffi` | `rs/moq-c`, `{py,swift,kt,dart}/`, `go/wrapper/moq/*.go` (the `go/ffi` and `dart/moq_ffi` bindings regenerate automatically, but a new method needs a hand-written wrapper too, like `py/moq-rs` or `dart/moq`), `cpp/moq`, `cpp/obs/src`, `doc/lib/{py,swift,kt,go,dart,c,cpp}`, `doc/bin/obs.md` | +| `rs/moq-ffi` | `c/moq`, `{py,swift,kt,dart}/`, `go/wrapper/moq/*.go` (the `go/ffi` and `dart/moq_ffi` bindings regenerate automatically, but a new method needs a hand-written wrapper too, like `py/moq-rs` or `dart/moq`), `cpp/moq`, `cpp/obs/src`, `doc/lib/{py,swift,kt,go,dart,c,cpp}`, `doc/bin/obs.md` | | `rs/moq-net` wire/API | `js/net`, `doc/concept`, `drafts/draft-lcurley-moq-lite.md` (if the wire spec changes) | | `rs/hang` catalog/container | `js/hang`, `doc/concept`, `drafts/draft-lcurley-moq-hang.md` (if the format spec changes) | | `rs/moq-auth` | `js/auth`, `doc/bin/relay/auth.md` | @@ -114,7 +114,7 @@ The `quest` binary comes from the kixelated/quest flake input and serves the que | `rs/moq-relay` config/behavior | `doc/bin/relay/` | | `rs/moq-cli` | `doc/bin/cli.md` | | `rs/moq-gst` | `doc/bin/gstreamer.md` | -| `rs/moq-c` C ABI (`moq.h`) | `doc/lib/c` | +| `c/moq` C API (`moq/moq.h`) | `doc/lib/c` | | `js/{watch,publish}` UI/API | `demo/web` if it consumes the API | | a kramdown-rfc construct new to `drafts/` | `doc/.vitepress/drafts.ts`, which translates the drafts into `/draft/` site pages | diff --git a/c/justfile b/c/justfile new file mode 100644 index 00000000000..136ac0056ea --- /dev/null +++ b/c/justfile @@ -0,0 +1,28 @@ +#!/usr/bin/env just --justfile +# +# The moq C package over rs/moq-ffi. Invoked from the repo root as `just c ` via the +# `mod c` import. + +set working-directory := '.' + +default: + just check + +# Build the package, or check an archive, and run its installed consumers. +[no-cd] +[positional-arguments] +check *args: + {{ quote(justfile_directory() / "sh/c/check.sh") }} "$@" + +# Runs in the caller's directory, so a relative `--output` means what it would +# to the script itself. + +# Build and package the C package for release. +[no-cd] +[positional-arguments] +package *args: + {{ quote(justfile_directory() / "c/moq/build.sh") }} "$@" + +# Remove the package builds. +clean: + rm -rf moq/build diff --git a/c/moq/.gitignore b/c/moq/.gitignore new file mode 100644 index 00000000000..fe29fdcc969 --- /dev/null +++ b/c/moq/.gitignore @@ -0,0 +1,2 @@ +# `just c check` builds, installs, and tests here. +/build/ diff --git a/c/moq/CMakeLists.txt b/c/moq/CMakeLists.txt new file mode 100644 index 00000000000..ccc4367fb5f --- /dev/null +++ b/c/moq/CMakeLists.txt @@ -0,0 +1,215 @@ +cmake_minimum_required(VERSION 3.21) + +# The package version is human-owned, like cpp/moq/VERSION; release-c.yml tags c-v. +file(STRINGS "${CMAKE_CURRENT_LIST_DIR}/VERSION" _version LIMIT_COUNT 1) +if(NOT _version MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$") + message(FATAL_ERROR "Unsupported version '${_version}' in c/moq/VERSION (want x.y.z)") +endif() + +# The API is C, but its implementation is the generated C++ bindings, compiled here. +project(moq-c VERSION ${_version} LANGUAGES C CXX) + +set(_workspace "${CMAKE_CURRENT_SOURCE_DIR}/../..") +set(MOQ_FFI_DIR "" CACHE PATH "Extracted default-feature moq-ffi native package") +if(MOQ_FFI_DIR) + get_filename_component(MOQ_FFI_DIR "${MOQ_FFI_DIR}" ABSOLUTE) + file(READ "${MOQ_FFI_DIR}/manifest.json" _manifest) + foreach(_field version target features profile source) + string(JSON _input_${_field} GET "${_manifest}" ${_field}) + endforeach() + if(NOT _input_features STREQUAL "default" OR NOT _input_profile STREQUAL "release") + message(FATAL_ERROR "Prebuilt moq-ffi requires default features and release profile") + endif() + if(NOT _input_version MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+([-+][0-9A-Za-z.+-]+)?$") + message(FATAL_ERROR "Malformed prebuilt moq-ffi version: ${_input_version}") + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(_ffi_target aarch64-unknown-linux-gnu) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") + set(_ffi_target x86_64-unknown-linux-gnu) + endif() + elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(_ffi_target aarch64-apple-darwin) + elseif(MSVC AND CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|x86_64)$") + set(_ffi_target x86_64-pc-windows-msvc) + endif() + if(NOT DEFINED _ffi_target OR NOT _ffi_target STREQUAL _input_target) + message(FATAL_ERROR "Prebuilt moq-ffi target ${_input_target} does not match this host") + endif() + set(_cargo_target "${MOQ_FFI_DIR}/lib") + set(_profile "") +else() + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(_profile debug) + set(_cargo_flags "") + else() + set(_profile release) + set(_cargo_flags --release) + endif() + if(DEFINED ENV{CARGO_TARGET_DIR}) + set(_cargo_target "$ENV{CARGO_TARGET_DIR}") + else() + set(_cargo_target "${_workspace}/target") + endif() +endif() + +if(WIN32) + set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/moq_ffi.lib") + set(_metadata "${_cargo_target}/${_profile}/moq_ffi.dll") +elseif(APPLE) + set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/libmoq_ffi.a") + set(_metadata "${_cargo_target}/${_profile}/libmoq_ffi.dylib") +else() + set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/libmoq_ffi.a") + set(_metadata "${_cargo_target}/${_profile}/libmoq_ffi.so") +endif() +if(MOQ_FFI_DIR) + foreach(_input "${MOQ_FFI_LIB}" "${_metadata}" "${MOQ_FFI_DIR}/native-libs.txt") + if(NOT EXISTS "${_input}") + message(FATAL_ERROR "Missing prebuilt FFI input: ${_input}") + endif() + endforeach() + set(_build_ffi "") +else() + # The package ships the OpenH264 software fallback, like the C++ package and the prebuilt + # input do. + set(_build_ffi COMMAND cargo build --locked --package moq-ffi --features openh264 ${_cargo_flags}) + set(_ffi_byproducts "${MOQ_FFI_LIB}" "${_metadata}") +endif() + +find_program(UNIFFI_BINDGEN_CPP uniffi-bindgen-cpp REQUIRED) + +# The C backend renders the C header, its C++ implementation, and the C++ bindings that +# implementation calls. It needs no config: C names are moq-ffi's own. Metadata comes from the cdylib, like the C++ package, while linking +# uses the staticlib. Rendered to a scratch directory and copied only when changed, so a no-op +# cargo build does not recompile the bindings. +set(_generated "${CMAKE_CURRENT_BINARY_DIR}/include/moq/ffi") +set(_generated_files moq.h moq_c.cpp moq.hpp moq.cpp moq_scaffolding.hpp uniffi_expected.hpp) +list(TRANSFORM _generated_files PREPEND "${_generated}/" OUTPUT_VARIABLE _generated_paths) +set(_render "${CMAKE_CURRENT_BINARY_DIR}/render") +list(TRANSFORM _generated_files PREPEND "${_render}/" OUTPUT_VARIABLE _render_paths) +# Named apart from cpp/moq's `moq_ffi`, so both packages can build in one tree. +add_custom_target(moq_c_ffi ALL + ${_build_ffi} + COMMAND ${CMAKE_COMMAND} -E rm -rf "${_render}" + COMMAND "${UNIFFI_BINDGEN_CPP}" --lang c --library "${_metadata}" --out-dir "${_render}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${_generated}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${_render_paths} "${_generated}" + WORKING_DIRECTORY "${_workspace}" + BYPRODUCTS ${_ffi_byproducts} ${_generated_paths} + COMMENT "Preparing moq-ffi and rendering its C bindings" + VERBATIM +) + +# System libraries the staticlib needs from an external linker. moq-ffi and moq-c share one +# dependency tree, so they share rs/moq-c/native-libs/, which says how to regenerate it. +if(APPLE) + set(_platform apple) +elseif(WIN32) + set(_platform windows) +else() + set(_platform linux) +endif() +if(MOQ_FFI_DIR) + set(_native_libs "${MOQ_FFI_DIR}/native-libs.txt") +else() + set(_native_libs "${_workspace}/rs/moq-c/native-libs/${_platform}.txt") +endif() +file(STRINGS "${_native_libs}" _lines) +set(MOQ_NATIVE_LIBS "") +foreach(_line IN LISTS _lines) + string(STRIP "${_line}" _line) + if(_line STREQUAL "" OR _line MATCHES "^#") + continue() + elseif(_line MATCHES "^framework:(.+)$") + list(APPEND MOQ_NATIVE_LIBS "-framework ${CMAKE_MATCH_1}") + else() + list(APPEND MOQ_NATIVE_LIBS "${_line}") + endif() +endforeach() + +# What a C linker must add for the C++ implementation: the C++ compiler's runtime libraries +# that the C compiler does not already link, such as libstdc++ or libc++. Empty on MSVC, whose +# objects name their runtime themselves. +# Whatever the native libraries already name, such as OpenH264's C++ runtime, is not repeated. +set(MOQ_CXX_RUNTIME ${CMAKE_CXX_IMPLICIT_LINK_LIBRARIES}) +foreach(_lib IN LISTS CMAKE_C_IMPLICIT_LINK_LIBRARIES MOQ_NATIVE_LIBS) + list(REMOVE_ITEM MOQ_CXX_RUNTIME "${_lib}") +endforeach() +list(REMOVE_DUPLICATES MOQ_CXX_RUNTIME) + +# The C API: the generated implementation plus src/, as one static library beside moq-ffi's. +# Named moq-c so an in-tree build can sit beside the C++ package's `moq-cpp` target. +add_library(moq-c STATIC + src/shutdown.c + "${_generated}/moq.cpp" + "${_generated}/moq_c.cpp" +) +add_library(moq::c ALIAS moq-c) +add_dependencies(moq-c moq_c_ffi) +set_target_properties(moq-c PROPERTIES + OUTPUT_NAME moq + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + # The Rust staticlib links the release CRT (/MD) in every configuration, so the C++ objects + # built with it do too. + MSVC_RUNTIME_LIBRARY MultiThreadedDLL +) +target_compile_features(moq-c PUBLIC c_std_11) +target_include_directories(moq-c PUBLIC + "$" + "$" +) +find_package(Threads REQUIRED) +target_link_libraries(moq-c PUBLIC "${MOQ_FFI_LIB}" ${MOQ_NATIVE_LIBS} ${MOQ_CXX_RUNTIME} Threads::Threads) + +if(PROJECT_IS_TOP_LEVEL) + include(GNUInstallDirs) + include(CMakePackageConfigHelpers) + + # moq-c.pc locates the prefix relative to itself, so the archive can be unpacked anywhere. + foreach(_dir CMAKE_INSTALL_LIBDIR CMAKE_INSTALL_INCLUDEDIR) + if(IS_ABSOLUTE "${${_dir}}") + message(FATAL_ERROR "${_dir} must be relative to the prefix, not ${${_dir}}") + endif() + endforeach() + file(RELATIVE_PATH MOQ_PC_PREFIX "/prefix/${CMAKE_INSTALL_LIBDIR}/pkgconfig" "/prefix") + string(REGEX REPLACE "/$" "" MOQ_PC_PREFIX "${MOQ_PC_PREFIX}") + + install(TARGETS moq-c ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + install(FILES include/moq/moq.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/moq) + install(FILES "${_generated}/moq.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/moq/ffi) + install(FILES "${MOQ_FFI_LIB}" DESTINATION ${CMAKE_INSTALL_LIBDIR}) + install(FILES MIGRATE.md DESTINATION ${CMAKE_INSTALL_DATADIR}/moq-c) + + # The config takes lists; pkg-config wants linker flags. + get_filename_component(MOQ_FFI_LIB_NAME "${MOQ_FFI_LIB}" NAME) + set(MOQ_C_LIB_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}moq${CMAKE_STATIC_LIBRARY_SUFFIX}") + set(MOQ_LIBS_PC "") + foreach(_lib IN LISTS MOQ_NATIVE_LIBS MOQ_CXX_RUNTIME) + if(_lib MATCHES "^-" OR IS_ABSOLUTE "${_lib}") + string(APPEND MOQ_LIBS_PC " ${_lib}") + else() + string(APPEND MOQ_LIBS_PC " -l${_lib}") + endif() + endforeach() + + configure_package_config_file(cmake/moq-c-config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/moq-c-config.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/moq-c + PATH_VARS CMAKE_INSTALL_INCLUDEDIR CMAKE_INSTALL_LIBDIR + ) + write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/moq-c-config-version.cmake" + COMPATIBILITY SameMinorVersion + ) + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/moq-c-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/moq-c-config-version.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/moq-c + ) + + configure_file(cmake/moq-c.pc.in "${CMAKE_CURRENT_BINARY_DIR}/moq-c.pc" @ONLY) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/moq-c.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +endif() diff --git a/c/moq/MIGRATE.md b/c/moq/MIGRATE.md new file mode 100644 index 00000000000..780f696142c --- /dev/null +++ b/c/moq/MIGRATE.md @@ -0,0 +1,199 @@ +# Migrating moq-c + +How to update your code for each breaking change, newest first. + +## Unreleased + +### The C API is generated from moq-ffi (0.8.0) + +moq-c 0.8.0 replaces the hand-written ABI with one generated from moq-ffi, the crate every other binding wraps, so a function here means what its method means in Python, Go, or C++. +Every call changes shape once; the names are moq-ffi's in snake case, and `` documents each one. + +| 0.7 | 0.8 | +| --- | --- | +| `#include ` | `#include ` | +| `-lmoq` | `-lmoq -lmoq_ffi` plus the C++ runtime; take them from `pkg-config --libs moq-c` or `moq::c` | +| `int32_t` handles | opaque pointers, each released with its `moq__free` | +| a negative return code, then `moq_error()` | a returned `moq_error *` (NULL on success) with `tag`, its fields, and `moq_error_message()`; free it with `moq_error_free` | +| results written to `dst` | results written to the last argument, `out` | +| `const char *` plus a length | a NUL-terminated UTF-8 string | +| a zeroed config struct | `moq__default()`, then set fields | +| a value plus a `*_present` flag | `{ has_value, value }`, or a pointer that is NULL when absent | +| one `moq_status_callback` per stream of results | a callback per call that returns a `moq_task *`; call again for the next result, and `moq_task_free` cancels | +| callbacks on moq's callback thread | callbacks on moq's dispatcher thread, or your own loop through `moq_set_dispatcher` and `moq_work_run` | +| process teardown | `moq_shutdown()` before unloading | + +A few common calls: + +| 0.7 | 0.8 | +| --- | --- | +| `moq_origin_create` | `moq_origin_producer_new` | +| `moq_origin_create_broadcast` | `moq_origin_producer_create_broadcast` | +| `moq_publish_track` | `moq_broadcast_producer_publish_track` | +| `moq_session_connect` | `moq_client_new`, then `moq_client_connect` | +| `moq_server_listen` | `moq_server_new`, `moq_server_listen`, then `moq_server_accept` per session | +| `moq_consume_track` | `moq_broadcast_consumer_subscribe_track`, then `moq_track_consumer_read_frame` per frame | + +```c +static void on_frame(void *user_data, moq_error *error, moq_frame *frame) { + if (error) { + fprintf(stderr, "read failed: %s\n", moq_error_message(error)); + moq_error_free(error); + } else if (frame) { + use(frame->payload.data, frame->payload.len); + moq_frame_free(frame); + } +} + +moq_task *task = moq_track_consumer_read_frame(track, on_frame, user_data); +// Free it after the callback, or before to cancel the read. +``` + +The archive holds the compiled C API (`libmoq.a` / `moq.lib`) and moq-ffi's staticlib (`libmoq_ffi.a` / `moq_ffi.lib`), and links the C++ runtime. +It embeds the same Rust runtime as the C++ package, `moq-cpp`, so a program links one of the two, never both. + +## moq-c 0.7, the hand-written ABI + + +### Demand reports the wanted group (#5930) + +`moq_track_info` gains `refresh` and `refresh_present`, which changes its ABI; a zeroed field leaves refresh off. +A demand watcher's `on_demand` now repeats `MOQ_DEMAND_USED` whenever the wanted group moves, so treat it as "read the wanted group" rather than an edge. +Read it with `moq_publish_demand_wanted`, and start a new group (a keyframe) once it passes the largest group written. +Before any group is written, the first subscriber wants group 0, so start one whenever it is present. + +```c +// `written` is false until a group is written; `largest` is the largest group written. +uint64_t group; +bool present; +if (moq_publish_demand_wanted(watcher, &group, &present) == 0 && present && (!written || group > largest)) start_group(); +``` + +### Key logging is opt-in (#5907) + +`SSLKEYLOGFILE` alone no longer writes TLS session secrets. +Set `moq_client_config::quic_keylog` or `moq_server_config::quic_keylog` as well to capture them, each a field appended to its struct; without `SSLKEYLOGFILE` the dial or listen fails. + +### OpenH264 is a build feature (#5771) + +The release archives and the CMake build still carry the software H.264 fallback. +A bare `cargo build -p moq-c` leaves it out; add `--features openh264` to keep it. + +### Subscriptions carry group ranges (#5732) + +`moq_subscription`'s `group_start`, `group_end`, and their `_present` flags are replaced by `ranges` and `ranges_len`, an array of `moq_range` whose ends are `moq_anchor`s, and `order` (`moq_order`), which changes its ABI. + +### Receive estimates come from `moq_session_recv_bandwidth` (#5777) + +`moq_connection_stats` drops `estimated_recv_rate_bps` and `estimated_recv_rate_valid`, which changes its ABI. +Hold the consumer `moq_session_recv_bandwidth` returns to request PROBE, read it with `moq_bandwidth_consumer_peek` or wait with `moq_bandwidth_consumer_changed`, and release it with `moq_bandwidth_consumer_close`. + +```c +int32_t consumer = moq_session_recv_bandwidth(session); // 0 when the version has no PROBE +if (consumer > 0) { + uint64_t bps; + bool present; + if (moq_bandwidth_consumer_peek(consumer, &bps, &present) == 0 && present) use(bps); + moq_bandwidth_consumer_close(consumer); +} +``` + +### `moq_origin_announced_broadcast` delivers every publisher instance (#5700) + +Its callback gets a new broadcast handle each time another instance takes over the path or the broadcast returns, and its terminal `0` only on cancel. +Free each handle with `moq_consume_close` once its successor arrives. + +### A timed track refuses a group that rewinds (#5803) + +A frame write fails with a protocol error of kind `MOQ_PROTOCOL_KIND_TIMESTAMP_MISMATCH` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + +### libmoq is moq-c (#4288) + +The C bindings ship as the `moq-c` crate, starting at 0.7.0. +The header is still `moq.h` and the library is still `libmoq.a` / `moq.lib` (`-lmoq`); the C ABI did not change with the rename. +The CMake package, pkg-config name, release tags, and archives follow the new name. + +| Before | After | +| --- | --- | +| `find_package(moq)`, target `moq` | `find_package(moq-c)`, target `moq::c` | +| `pkg-config --libs moq` | `pkg-config --libs moq-c` | +| nix `.#libmoq` | nix `.#moq-c` | +| tags `libmoq-v*`, archives `libmoq--` | tags `moq-c-v*`, archives `moq-c--` | + +### Background panics are reported (#5562) + +A panic in a background task reports `MOQ_ERROR_PANIC`, and other handles keep running. + +### Announcements carry a kind (#4266, #4384, #5087) + +`moq_announce_update` is `moq_announce_event`, and its `active` flag is a `kind`: `MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, `_END`, or `_RESTART`. +`_RESTART` means another publisher instance now serves the prefix: request the path again. +Subscribe on `MOQ_ANNOUNCE_KIND_START` or `_RESTART` where you checked `active`. + +### `moq_origin_announced` takes a config (#4111) + +The prefix and filter arguments moved into a `moq_announce_config`; pass `NULL` to receive everything. + +```c +moq_announce_config config = { .prefix = prefix, .prefix_len = prefix_len }; +moq_origin_announced(origin, &config, on_announce, user_data); +``` + +### Frames say whether they are timed + +`moq_frame` and `moq_datagram` gain `timestamp_present`, which changes their size, so recompile against the new `moq.h`. +A frame read from an untimed track has `timestamp_present` false. + +```c +if (frame.timestamp_present) use(frame.timestamp_us); +``` + +### Subscriber staleness is `max_delay_us` + +`moq_subscription`, `moq_audio_decoder_output`, `moq_video_decoder_output`, `moq_consume_video`, and `moq_consume_audio` take `max_delay_us` instead of `max_age_us`. + +### `stalled` is `enabled` (#4915) + +`moq_consume_video_stalled` is `moq_consume_video_enabled`, with the meaning inverted. + +### Routes have one cost (#4661) + +`moq_route.cold` and `has_cold` are removed; drop them. + +### `moq_publish_finish` is removed (#4152) + +Close the broadcast with `moq_publish_close`. + +### A decoded frame can fail on its own (#4094) + +`moq_decode_video_frame` can return a conversion error for a single frame; it used to end the stream at the terminal callback. + +## libmoq 0.5 -> 0.6 + +### One client config + +The 41 `moq_client_*` setters are one zero-initializable `moq_client_config`, whose durations are `_us`, passed to `moq_session_connect` instead of `moq_client_connect`. + +```c +moq_client_config config = { .tls_disable_verify = true }; +moq_session_connect(url, url_len, &config, origin, 0, on_status, user_data); +``` + +### Publish split by kind + +`moq_publish_media` is `moq_publish_audio`, `moq_publish_video`, or `moq_publish_container`, and the raw encoders are `moq_encode_audio*` / `moq_encode_video*`. +Formats are enums instead of strings. + +```c +moq_audio_init init = { .format = MOQ_AUDIO_FORMAT_OPUS, .init = opus_head, .init_len = opus_head_len }; +int track = moq_publish_audio(broadcast, &init); +``` + +### Announcement renames + +| Before | After | +| --- | --- | +| `moq_announced` | `moq_announce_update` | +| `moq_origin_consume_announced` | `moq_origin_announced_broadcast` | +| `moq_broadcast_request_abort` | `moq_broadcast_request_reject` | diff --git a/c/moq/README.md b/c/moq/README.md new file mode 100644 index 00000000000..03bbae4c75e --- /dev/null +++ b/c/moq/README.md @@ -0,0 +1,23 @@ +# moq C package + +The C API for [rs/moq-ffi](../../rs/moq-ffi), generated by the C backend of `uniffi-bindgen-cpp`, plus `include/moq/moq.h`, which adds the one call the generator cannot: `moq_shutdown`. Nothing here re-implements a function; a shape problem is fixed in moq-ffi, where every binding benefits. User docs: [doc/lib/c](../../doc/lib/c/index.md). Migrating from the hand-written 0.7 ABI: [MIGRATE.md](MIGRATE.md). + +## Layout + +- `CMakeLists.txt` builds moq-ffi as a staticlib with cargo, or consumes an explicit `MOQ_FFI_DIR` native input, renders the C header and its C++ implementation into the build tree, and compiles them as the `moq::c` target. Use it with `add_subdirectory`, or install it for `find_package(moq-c)` and `moq-c.pc`. +- `src/shutdown.c` implements `moq_shutdown`: the moq-ffi runtime workers, then the dispatcher. +- `cmake/` holds the installed CMake package and pkg-config templates. Both locate the prefix relative to themselves and honor `CMAKE_INSTALL_LIBDIR`. +- `build.sh` builds the release archive `release-c.yml` publishes. `--ffi-dir DIR` selects an extracted default-feature native package; otherwise the C++ package's `FFI_VERSION` pin selects one, the same input for both. +- `test/probe.c` exercises the installed package from a C-only project over a real QUIC session, with every callback run on its own loop through `moq_set_dispatcher`. + +## Checking + +`just c check` builds the package, installs it to a `lib64` prefix, builds the probe against it through `find_package` and, outside Windows, through pkg-config, and runs each. `just c check --archive FILE` runs the same consumer checks against an extracted release archive. Release dry runs check every archive this way. + +The generator is the one the C++ package pins; see [cpp/moq](../../cpp/moq/README.md) to install it without Nix. `flake.nix` lists every place that names its tag. + +## Shape + +The implementation is C++17 over the generated C++ bindings, compiled into `libmoq.a` (`moq.lib`) beside moq-ffi's `libmoq_ffi.a` (`moq_ffi.lib`), so a C program needs no C++ compiler, but its link adds the C++ runtime, which the CMake package and `moq-c.pc` name. Both libraries use the release CRT (`/MD`) on MSVC in every configuration. + +This package and the C++ package, `moq-cpp`, each embed moq-ffi, so one program links one of them, never both, and they install to separate prefixes. diff --git a/c/moq/VERSION b/c/moq/VERSION new file mode 100644 index 00000000000..a3df0a6959e --- /dev/null +++ b/c/moq/VERSION @@ -0,0 +1 @@ +0.8.0 diff --git a/c/moq/build.sh b/c/moq/build.sh new file mode 100755 index 00000000000..a9f460a43e6 --- /dev/null +++ b/c/moq/build.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build the moq C package for the host and archive it for release. +# Usage: ./build.sh [--target TARGET] [--output DIR] [--ffi-dir DIR] +# +# The archive holds what `cmake --install` lays out: the compiled C API, the moq-ffi staticlib, +# the headers, the CMake package, moq-c.pc, and MIGRATE.md. Needs cargo, cmake, a C and C++ +# compiler, and uniffi-bindgen-cpp on PATH. The version is c/moq/VERSION. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +TARGET="" +OUTPUT_DIR="dist" +FFI_DIR="" + +while [[ $# -gt 0 ]]; do + case $1 in + --target) + TARGET="$2" + shift 2 + ;; + --ffi-dir) + FFI_DIR="$(cd "$2" && pwd)" + shift 2 + ;; + --output) + OUTPUT_DIR="$2" + shift 2 + ;; + -h | --help) + echo "Usage: $0 [--target TARGET] [--output DIR] [--ffi-dir DIR]" + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +VERSION=$(tr -d '[:space:]' <"$SCRIPT_DIR/VERSION") +HOST_TARGET=$(rustc -vV | sed -n 's/^host: //p') +TARGET="${TARGET:-$HOST_TARGET}" + +# CMake builds for the host. A mismatch would silently mislabel the archive. +if [[ "$TARGET" != "$HOST_TARGET" ]]; then + echo "Error: unsupported cross ($HOST_TARGET -> $TARGET); refusing to mislabel the archive." >&2 + exit 1 +fi + +NAME="moq-c-${VERSION}-${TARGET}" +mkdir -p "$OUTPUT_DIR" +OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" +PACKAGE_DIR="$OUTPUT_DIR/$NAME" +BUILD_DIR="$SCRIPT_DIR/build/release" + +# The C++ package's pin selects the same default-feature input for both packages. +if [[ -z "$FFI_DIR" ]] && sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' "$SCRIPT_DIR/../../cpp/moq/FFI_VERSION" | grep -q .; then + FFI_DIR="$BUILD_DIR/ffi" + rm -rf "$FFI_DIR" + python3 "$SCRIPT_DIR/../../sh/cpp/ffi.py" fetch --target "$TARGET" --output "$FFI_DIR" +fi + +echo "Packaging $NAME..." +rm -rf "$PACKAGE_DIR" +# lib/, not the lib64/ GNUInstallDirs picks on some hosts, so every archive has one layout. +cmake -S "$SCRIPT_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_LIBDIR=lib -DMOQ_FFI_DIR="$FFI_DIR" +cmake --build "$BUILD_DIR" --config Release +cmake --install "$BUILD_DIR" --config Release --prefix "$PACKAGE_DIR" + +# A placeholder the configure step missed would reach a consumer's linker as a literal. +# Comments are skipped: configure_package_config_file names @PACKAGE_INIT@ in one. +if grep -rnE '^[^#]*@[A-Z_]+@' "$PACKAGE_DIR/lib/cmake" "$PACKAGE_DIR/lib/pkgconfig"; then + echo "Error: unsubstituted placeholder in the package config (see above)" >&2 + exit 1 +fi + +cd "$OUTPUT_DIR" +if [[ "$TARGET" == *"-windows-"* ]]; then + ARCHIVE="$NAME.zip" + if command -v 7z &>/dev/null; then + 7z a "$ARCHIVE" "$NAME" + else + zip -r "$ARCHIVE" "$NAME" + fi +else + ARCHIVE="$NAME.tar.gz" + tar -czf "$ARCHIVE" "$NAME" +fi +rm -rf "$NAME" + +echo "Created: $OUTPUT_DIR/$ARCHIVE" diff --git a/c/moq/cmake/moq-c-config.cmake.in b/c/moq/cmake/moq-c-config.cmake.in new file mode 100644 index 00000000000..e5f9f7eb9f7 --- /dev/null +++ b/c/moq/cmake/moq-c-config.cmake.in @@ -0,0 +1,20 @@ +# find_package(moq-c): the C API for moq-ffi as the moq::c target. Every moq package exports +# into the one moq:: namespace, and the package names keep installs apart. + +@PACKAGE_INIT@ + +if(NOT TARGET moq::c) + find_package(Threads REQUIRED) + + # The compiled C API, then the prebuilt Rust staticlib and what an external linker must add + # for the two: the system libraries and the C++ runtime, baked in for the platform and + # compiler this package was built with. + add_library(moq::c STATIC IMPORTED) + set_target_properties(moq::c PROPERTIES + IMPORTED_LOCATION "@PACKAGE_CMAKE_INSTALL_LIBDIR@/@MOQ_C_LIB_NAME@" + INTERFACE_INCLUDE_DIRECTORIES "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@" + INTERFACE_LINK_LIBRARIES "@PACKAGE_CMAKE_INSTALL_LIBDIR@/@MOQ_FFI_LIB_NAME@;@MOQ_NATIVE_LIBS@;@MOQ_CXX_RUNTIME@;Threads::Threads" + ) +endif() + +check_required_components(moq-c) diff --git a/c/moq/cmake/moq-c.pc.in b/c/moq/cmake/moq-c.pc.in new file mode 100644 index 00000000000..8f8b2a0f5fb --- /dev/null +++ b/c/moq/cmake/moq-c.pc.in @@ -0,0 +1,9 @@ +prefix=${pcfiledir}/@MOQ_PC_PREFIX@ +libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: moq-c +Description: Media over QUIC for C +Version: @PROJECT_VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -lmoq -lmoq_ffi@MOQ_LIBS_PC@ diff --git a/c/moq/include/moq/moq.h b/c/moq/include/moq/moq.h new file mode 100644 index 00000000000..d68de623c75 --- /dev/null +++ b/c/moq/include/moq/moq.h @@ -0,0 +1,21 @@ +/* The moq C package: the C API generated from moq-ffi, plus the shutdown it cannot generate. */ +#ifndef MOQ_MOQ_H +#define MOQ_MOQ_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Stops the moq runtime workers, then the dispatcher, before the process or module goes away. + * Pending calls never call back. Call it once, from a thread that is not running a callback. + */ +void moq_shutdown(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/moq/src/shutdown.c b/c/moq/src/shutdown.c new file mode 100644 index 00000000000..e6aff879669 --- /dev/null +++ b/c/moq/src/shutdown.c @@ -0,0 +1,10 @@ +#include + +/* Exported by the moq-ffi staticlib, but not a uniffi export, so the generated header does not + * declare it. */ +void moq_ffi_shutdown(void); + +void moq_shutdown(void) { + moq_ffi_shutdown(); + moq_shutdown_dispatcher(); +} diff --git a/c/moq/test/CMakeLists.txt b/c/moq/test/CMakeLists.txt new file mode 100644 index 00000000000..5cdd6210661 --- /dev/null +++ b/c/moq/test/CMakeLists.txt @@ -0,0 +1,19 @@ +# A consumer of the installed package: `just c check` installs c/moq to a prefix, then builds +# this with find_package(moq-c). C only, so a C++ toolchain is not what makes it link. +cmake_minimum_required(VERSION 3.21) +project(moq-probe LANGUAGES C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +if(MSVC) + add_compile_options(/W4 /WX) +else() + add_compile_options(-Wall -Wextra -Werror -pedantic-errors) +endif() + +find_package(moq-c REQUIRED) + +add_executable(probe probe.c) +target_link_libraries(probe PRIVATE moq::c) diff --git a/c/moq/test/probe.c b/c/moq/test/probe.c new file mode 100644 index 00000000000..b6e00c655f2 --- /dev/null +++ b/c/moq/test/probe.c @@ -0,0 +1,365 @@ +/* Exercises the installed package end to end over a real QUIC session from plain C: a returned + * error, a loopback connect, a frame read through a callback, a cancelled read, and every + * callback run on this thread through a custom dispatcher. */ + +/* clock_gettime and the pthread timed wait under strict C11. */ +#define _POSIX_C_SOURCE 200809L + +#include + +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +/* Unlike assert, this survives a release build. */ +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #expr); \ + abort(); \ + } \ + } while (0) + +/* Unwraps a call the probe expects to succeed. */ +static void ok(moq_error *error, const char *what) { + if (error) { + fprintf(stderr, "%s failed: %s\n", what, moq_error_message(error)); + abort(); + } +} + +/* The host loop: a queue of work that only the main thread runs. */ +typedef struct node { + moq_work *work; + struct node *next; +} node; + +static struct { +#ifdef _WIN32 + SRWLOCK lock; + CONDITION_VARIABLE ready; +#else + pthread_mutex_t lock; + pthread_cond_t ready; +#endif + node *head; + node *tail; + bool closed; +} loop; + +static void loop_lock(void) { +#ifdef _WIN32 + AcquireSRWLockExclusive(&loop.lock); +#else + pthread_mutex_lock(&loop.lock); +#endif +} + +static void loop_unlock(void) { +#ifdef _WIN32 + ReleaseSRWLockExclusive(&loop.lock); +#else + pthread_mutex_unlock(&loop.lock); +#endif +} + +/* Waits up to `ms` for a signal; the caller holds the lock. */ +static void loop_wait(int ms) { +#ifdef _WIN32 + SleepConditionVariableSRW(&loop.ready, &loop.lock, (DWORD)ms, 0); +#else + struct timespec until; + clock_gettime(CLOCK_REALTIME, &until); + until.tv_sec += ms / 1000; + until.tv_nsec += (long)(ms % 1000) * 1000000L; + if (until.tv_nsec >= 1000000000L) { + until.tv_sec++; + until.tv_nsec -= 1000000000L; + } + pthread_cond_timedwait(&loop.ready, &loop.lock, &until); +#endif +} + +/* Called from any thread. */ +static bool loop_dispatch(void *user_data, moq_work *work) { + (void)user_data; + node *item = malloc(sizeof(node)); + CHECK(item); + item->work = work; + item->next = NULL; + loop_lock(); + if (loop.closed) { + loop_unlock(); + free(item); + return false; + } + if (loop.tail) { + loop.tail->next = item; + } else { + loop.head = item; + } + loop.tail = item; +#ifdef _WIN32 + WakeConditionVariable(&loop.ready); +#else + pthread_cond_signal(&loop.ready); +#endif + loop_unlock(); + return true; +} + +static node *loop_pop(void) { + node *item = loop.head; + if (item) { + loop.head = item->next; + if (!loop.head) { + loop.tail = NULL; + } + } + return item; +} + +/* Refuses new work and runs what was accepted. */ +static void loop_shutdown(void *user_data) { + (void)user_data; + loop_lock(); + loop.closed = true; + loop_unlock(); + for (;;) { + loop_lock(); + node *item = loop_pop(); + loop_unlock(); + if (!item) { + return; + } + moq_work_run(item->work); + free(item); + } +} + +/* Runs work on this thread until `*done` or roughly `ms` passes without it; returns `*done`. */ +static bool pump(const bool *done, int ms) { + int waited = 0; + while (!*done && waited < ms) { + loop_lock(); + node *item = loop_pop(); + if (!item) { + loop_wait(10); + waited += 10; + item = loop_pop(); + } + loop_unlock(); + if (item) { + moq_work_run(item->work); + free(item); + } + } + return *done; +} + +/* Each async result lands in one of these, on this thread. */ +typedef struct { + bool done; + moq_error *error; + void *value; +} result; + +static void on_value(void *user_data, moq_error *error, void *value) { + result *out = user_data; + out->done = true; + out->error = error; + out->value = value; +} + +static void on_string(void *user_data, moq_error *error, char *value) { + on_value(user_data, error, value); +} + +static void on_request(void *user_data, moq_error *error, moq_request *value) { + on_value(user_data, error, value); +} + +static void on_session(void *user_data, moq_error *error, moq_session *value) { + on_value(user_data, error, value); +} + +static void on_broadcast(void *user_data, moq_error *error, moq_broadcast_consumer *value) { + on_value(user_data, error, value); +} + +static void on_track(void *user_data, moq_error *error, moq_track_consumer *value) { + on_value(user_data, error, value); +} + +static void on_frame(void *user_data, moq_error *error, moq_frame *value) { + on_value(user_data, error, value); +} + +/* Waits for a result the probe expects to succeed and returns its value. */ +static void *await(result *pending, const char *what) { + if (!pump(&pending->done, 5000)) { + fprintf(stderr, "%s timed out\n", what); + abort(); + } + ok(pending->error, what); + return pending->value; +} + +int main(void) { +#ifdef _WIN32 + InitializeSRWLock(&loop.lock); + InitializeConditionVariable(&loop.ready); +#else + pthread_mutex_init(&loop.lock, NULL); + pthread_cond_init(&loop.ready, NULL); +#endif + moq_dispatcher dispatcher = {loop_dispatch, loop_shutdown, NULL}; + moq_set_dispatcher(&dispatcher); + + /* A synchronous error is a returned value with Rust's Display message. */ + moq_server_config unbound_config = moq_server_config_default(); + moq_server *unbound = NULL; + ok(moq_server_new(&unbound_config, &unbound), "server new"); + moq_string_list *fingerprints = NULL; + moq_error *error = moq_server_cert_fingerprints(unbound, &fingerprints); + CHECK(error && error->tag == MOQ_ERROR_BIND); + CHECK(strcmp(moq_error_message(error), "bind: not listening; call listen() first") == 0); + moq_error_free(error); + moq_server_free(unbound); + + /* Publisher: a server whose origin serves one broadcast with one track. */ + moq_origin_config origin_config = moq_origin_config_default(); + moq_origin_producer *origin = moq_origin_producer_new(&origin_config); + moq_broadcast_producer *broadcast = NULL; + ok(moq_origin_producer_create_broadcast(origin, "probe", &broadcast), "create_broadcast"); + moq_track_info info = moq_track_info_default(); + info.timescale.has_value = true; + info.timescale.value = 1000000; + moq_track_producer *track = NULL; + ok(moq_broadcast_producer_publish_track(broadcast, "data", &info, &track), "publish_track"); + moq_route route = moq_route_default(); + ok(moq_broadcast_producer_announce(broadcast, &route), "announce"); + + const char *generate[] = {"localhost"}; + moq_server_config server_config = moq_server_config_default(); + server_config.bind = "127.0.0.1:0"; + server_config.tls.generate.data = generate; + server_config.tls.generate.len = 1; + server_config.publish = origin; + moq_server *server = NULL; + ok(moq_server_new(&server_config, &server), "server new"); + result listening = {0}; + moq_task *task = moq_server_listen(server, on_string, &listening); + char *addr = await(&listening, "listen"); + moq_task_free(task); + + /* Both halves of the handshake are pending at once, and complete on this thread. */ + moq_client_config client_config = moq_client_config_default(); + client_config.tls.insecure = true; + moq_client *client = NULL; + ok(moq_client_new(&client_config, &client), "client new"); + char url[128]; + snprintf(url, sizeof(url), "https://%s", addr); + moq_string_free(addr); + result accepting = {0}; + result connecting = {0}; + moq_task *accept_task = moq_server_accept(server, on_request, &accepting); + moq_task *connect_task = moq_client_connect(client, url, on_session, &connecting); + moq_request *request = await(&accepting, "accept"); + CHECK(request); + result serving = {0}; + moq_task *serve_task = moq_request_accept(request, NULL, NULL, on_session, &serving); + moq_session *served = await(&serving, "request accept"); + moq_session *session = await(&connecting, "connect"); + moq_task_free(accept_task); + moq_task_free(connect_task); + moq_task_free(serve_task); + + /* Subscriber: resolve the announced broadcast and subscribe to its track. */ + moq_origin_consumer *remote_origin = moq_session_consume(session); + moq_announced_broadcast *announced = NULL; + ok(moq_origin_consumer_announced_broadcast(remote_origin, "probe", &announced), "announced_broadcast"); + result available = {0}; + task = moq_announced_broadcast_available(announced, on_broadcast, &available); + moq_broadcast_consumer *remote = await(&available, "available"); + moq_task_free(task); + result subscribed = {0}; + task = moq_broadcast_consumer_subscribe_track(remote, "data", NULL, on_track, &subscribed); + moq_track_consumer *consumer = await(&subscribed, "subscribe_track"); + moq_task_free(task); + + /* Freeing a pending task cancels the read, and its callback never runs. */ + result cancelled = {0}; + task = moq_track_consumer_read_frame(consumer, on_frame, &cancelled); + CHECK(!pump(&cancelled.done, 50)); + moq_task_free(task); + + /* The consumer survives the cancelled read and delivers the next frame. */ + result reading = {0}; + task = moq_track_consumer_read_frame(consumer, on_frame, &reading); + const uint8_t hello[] = {'h', 'e', 'l', 'l', 'o'}; + moq_frame frame = moq_frame_default(); + frame.payload.data = hello; + frame.payload.len = sizeof(hello); + frame.timestamp_us.has_value = true; + frame.timestamp_us.value = 1000; + ok(moq_track_producer_write_frame(track, &frame), "write_frame"); + moq_frame *received = await(&reading, "read_frame"); + moq_task_free(task); + CHECK(received); + CHECK(received->payload.len == sizeof(hello)); + CHECK(memcmp(received->payload.data, hello, sizeof(hello)) == 0); + CHECK(received->timestamp_us.has_value && received->timestamp_us.value == 1000); + moq_frame_free(received); + CHECK(!cancelled.done); + + /* Reading past a finished track delivers no frame. */ + ok(moq_track_producer_finish(track), "track finish"); + result ended = {0}; + task = moq_track_consumer_read_frame(consumer, on_frame, &ended); + CHECK(await(&ended, "read_frame at end") == NULL); + moq_task_free(task); + + /* An async error arrives through the callback as a value too. */ + moq_client *closed = NULL; + ok(moq_client_new(&client_config, &closed), "client new"); + moq_client_cancel(closed); + result refused = {0}; + task = moq_client_connect(closed, url, on_session, &refused); + CHECK(pump(&refused.done, 5000)); + CHECK(refused.error && refused.error->tag == MOQ_ERROR_CANCELLED); + moq_error_free(refused.error); + moq_task_free(task); + moq_client_free(closed); + + ok(moq_broadcast_producer_close(broadcast), "broadcast close"); + moq_session_cancel(session, 0); + moq_session_cancel(served, 0); + moq_server_cancel(server); + + moq_track_consumer_free(consumer); + moq_broadcast_consumer_free(remote); + moq_announced_broadcast_free(announced); + moq_origin_consumer_free(remote_origin); + moq_session_free(session); + moq_session_free(served); + moq_request_free(request); + moq_client_free(client); + moq_server_free(server); + moq_track_producer_free(track); + moq_broadcast_producer_free(broadcast); + moq_origin_producer_free(origin); + + /* Stop the runtime and the dispatcher before the process tears down. */ + moq_shutdown(); + + printf("probe: ok\n"); + return 0; +} diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md index aa0a5afeef6..efe1e4b9561 100644 --- a/cpp/moq/MIGRATE.md +++ b/cpp/moq/MIGRATE.md @@ -4,6 +4,10 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `AudioEncoderFinish` is `AudioEncoderFinished` (#PR) + +`moq::AudioEncoder::finish` returns the renamed record; its fields are unchanged. + ### Track demand reports the wanted group (#5930) `TrackDemand::is_used`, `used`, and `unused` are gone. diff --git a/dart/moq/lib/audio.dart b/dart/moq/lib/audio.dart index 6eace5e0c6a..fd12f227a6d 100644 --- a/dart/moq/lib/audio.dart +++ b/dart/moq/lib/audio.dart @@ -35,7 +35,7 @@ typedef Frame = MoqAudioFrame; typedef Packet = MoqAudioPacket; /// The packets an [Encoder] drains on finish and the padded samples to discard. -typedef EncoderFinish = MoqAudioEncoderFinish; +typedef EncoderFinish = MoqAudioEncoderFinished; /// The audio codec: `Codec.opus()` or `Codec.aac()`. typedef Codec = MoqAudioCodec; diff --git a/dart/moq_ffi/lib/src/moq.dart b/dart/moq_ffi/lib/src/moq.dart index 2947697cf54..a1e624aac5f 100644 --- a/dart/moq_ffi/lib/src/moq.dart +++ b/dart/moq_ffi/lib/src/moq.dart @@ -121,18 +121,21 @@ class FfiConverterMoqAudioDecoderOutput { } } -class MoqAudioEncoderFinish { +class MoqAudioEncoderFinished { final List packets; final int discardPadding; - MoqAudioEncoderFinish({required this.packets, required this.discardPadding}); + MoqAudioEncoderFinished({ + required this.packets, + required this.discardPadding, + }); } -class FfiConverterMoqAudioEncoderFinish { - static MoqAudioEncoderFinish lift(RustBuffer buf) { - return FfiConverterMoqAudioEncoderFinish.read(buf.asUint8List()).value; +class FfiConverterMoqAudioEncoderFinished { + static MoqAudioEncoderFinished lift(RustBuffer buf) { + return FfiConverterMoqAudioEncoderFinished.read(buf.asUint8List()).value; } - static LiftRetVal read(Uint8List buf) { + static LiftRetVal read(Uint8List buf) { int new_offset = buf.offsetInBytes; final packets_lifted = FfiConverterSequenceMoqAudioPacket.read( Uint8List.view(buf.buffer, new_offset), @@ -145,12 +148,12 @@ class FfiConverterMoqAudioEncoderFinish { final discardPadding = discardPadding_lifted.value; new_offset += discardPadding_lifted.bytesRead; return LiftRetVal( - MoqAudioEncoderFinish(packets: packets, discardPadding: discardPadding), + MoqAudioEncoderFinished(packets: packets, discardPadding: discardPadding), new_offset - buf.offsetInBytes, ); } - static RustBuffer lower(MoqAudioEncoderFinish value) { + static RustBuffer lower(MoqAudioEncoderFinished value) { final total_length = FfiConverterSequenceMoqAudioPacket.allocationSize(value.packets) + FfiConverterUInt64.allocationSize(value.discardPadding) + @@ -160,7 +163,7 @@ class FfiConverterMoqAudioEncoderFinish { return toRustBuffer(buf); } - static int write(MoqAudioEncoderFinish value, Uint8List buf) { + static int write(MoqAudioEncoderFinished value, Uint8List buf) { int new_offset = buf.offsetInBytes; new_offset += FfiConverterSequenceMoqAudioPacket.write( value.packets, @@ -173,7 +176,7 @@ class FfiConverterMoqAudioEncoderFinish { return new_offset - buf.offsetInBytes; } - static int allocationSize(MoqAudioEncoderFinish value) { + static int allocationSize(MoqAudioEncoderFinished value) { return FfiConverterSequenceMoqAudioPacket.allocationSize(value.packets) + FfiConverterUInt64.allocationSize(value.discardPadding) + 0; @@ -7422,7 +7425,7 @@ class FfiConverterMoqAudioConsumer { abstract class MoqAudioEncoderInterface { MoqAudioPacket encode({required List pcm}); - MoqAudioEncoderFinish finish({required List pcm}); + MoqAudioEncoderFinished finish({required List pcm}); int frameSize(); void setBitrate({required int bitrate}); MoqAudioInit trackInit(); @@ -7481,14 +7484,14 @@ class MoqAudioEncoder implements MoqAudioEncoderInterface { ); } - MoqAudioEncoderFinish finish({required List pcm}) { + MoqAudioEncoderFinished finish({required List pcm}) { return rustCallWithLifter( (status) => uniffi_moq_ffi_fn_method_moqaudioencoder_finish( uniffiClonePointer(), FfiConverterSequenceDouble32.lower(pcm), status, ), - FfiConverterMoqAudioEncoderFinish.lift, + FfiConverterMoqAudioEncoderFinished.lift, moqExceptionErrorHandler, ); } @@ -18346,7 +18349,7 @@ void _checkApiChecksums() { if (uniffi_moq_ffi_checksum_method_moqaudioencoder_encode() != 56589) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } - if (uniffi_moq_ffi_checksum_method_moqaudioencoder_finish() != 62112) { + if (uniffi_moq_ffi_checksum_method_moqaudioencoder_finish() != 24341) { throw UniffiInternalError.panicked("UniFFI API checksum mismatch"); } if (uniffi_moq_ffi_checksum_method_moqaudioencoder_frame_size() != 37264) { diff --git a/doc/index.md b/doc/index.md index 5997d10855d..eca8fd76afb 100644 --- a/doc/index.md +++ b/doc/index.md @@ -146,7 +146,7 @@ Some highlights: FFI bindings around the Rust core, with idiomatic APIs in each language: - [C++](/lib/cpp/) - RAII objects and cancellable futures, `co_await`-able on C++20. -- [C](/lib/c/) - `moq-c` static + shared library with an auto-generated header. +- [C](/lib/c/) - `moq-c` static library with an auto-generated header. - [Python](/lib/py/) - `asyncio`-friendly bindings, published to PyPI. - [Kotlin](/lib/kt/) - Coroutines and `Flow` for Android and the JVM. - [Swift](/lib/swift/) - Async sequences for iOS, iPadOS, and macOS. diff --git a/doc/lib/c/index.md b/doc/lib/c/index.md index b04a67ba4a7..ed2c9cbb38d 100644 --- a/doc/lib/c/index.md +++ b/doc/lib/c/index.md @@ -36,6 +36,16 @@ target, and `nix build .#moq-c` produces the release layout. A bare pkg-config file, and omits the OpenH264 software fallback unless given `--features openh264`. +`moq-c` and the C++ package each link in their own copy of the Rust core, so a +program links one of the two, never both. + +The next release, 0.8, replaces this ABI with a C API generated from +`moq-ffi`, the crate every other binding wraps. It installs as `` +with the same `moq::c` target and `moq-c.pc`, and its +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/c/moq/MIGRATE.md) maps +each call. To try it from a checkout, `just c package` builds the archive from +[`c/moq`](https://github.com/moq-dev/moq/tree/main/c/moq). + ## Connect ```c diff --git a/doc/lib/cpp/index.md b/doc/lib/cpp/index.md index 876392fbd12..9a70b1581e1 100644 --- a/doc/lib/cpp/index.md +++ b/doc/lib/cpp/index.md @@ -51,7 +51,8 @@ an explicit directory, a release pin in `cpp/moq/FFI_VERSION` selects the defaul input; `just cpp package` fetches it with the checked-in SHA-256 hashes and fails if it cannot verify it. Direct CMake builds and `just cpp check` build the current checkout unless given an explicit native directory. The package is -`moq-cpp`, so it installs beside the C package, `moq-c`, without colliding. +`moq-cpp`. It and the C package, `moq-c`, each link in their own copy of the +Rust core, so a program links one of the two, never both. ## Example diff --git a/flake.nix b/flake.nix index 8a8c51cc2d3..09fb32945e1 100644 --- a/flake.nix +++ b/flake.nix @@ -361,17 +361,18 @@ doCheck = false; }; - # uniffi-bindgen-cpp renders rs/moq-ffi for cpp/moq's CMake build. Not in - # nixpkgs, so build it from source; without it `just check` skips the - # cpp module, which MOQ_STRICT turns into a failure in CI. + # uniffi-bindgen-cpp renders rs/moq-ffi for the CMake builds of cpp/moq and, + # with its C backend, c/moq. Not in nixpkgs, so build it from source; + # without it `just check` skips the c and cpp modules, which MOQ_STRICT + # turns into a failure in CI. # # Like uniffi-bindgen-go, the tag pairs the generator's version with the # uniffi release it reads, so it moves with the `uniffi` dependency in # rs/moq-ffi/Cargo.toml. The other places name the same tag and must be # bumped together: the `cargo install` lines in rs/moq-ffi/build.sh, - # cpp/moq/README.md, .github/workflows/cpp.yml, - # .github/workflows/platform.yml, .github/workflows/release-cpp.yml, and - # .github/workflows/release-shared-ffi.yml. + # cpp/moq/README.md, .github/workflows/{c,cpp}.yml, + # .github/workflows/platform.yml, .github/workflows/release-{c,cpp}.yml, + # and .github/workflows/release-shared-ffi.yml. # # This points at a fork of LiveKit's async branch (livekit/uniffi-bindgen-cpp # PR #1): neither LiveKit nor NordSecurity has a uniffi 0.32 generator, diff --git a/go/wrapper/audio/audio.go b/go/wrapper/audio/audio.go index 2966e3ed39c..a69b8595869 100644 --- a/go/wrapper/audio/audio.go +++ b/go/wrapper/audio/audio.go @@ -27,7 +27,7 @@ type EncoderInput = ffi.MoqAudioEncoderInput type Packet = ffi.MoqAudioPacket // EncoderFinish holds the packets Encoder.Finish drains and the padded samples to discard. -type EncoderFinish = ffi.MoqAudioEncoderFinish +type EncoderFinish = ffi.MoqAudioEncoderFinished // Preset is how the encoder trades latency for compression; a nil EncoderOutput.Preset is PresetBalanced. type Preset = ffi.MoqAudioPreset diff --git a/justfile b/justfile index 0a5b1309523..aa9f326a925 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,8 @@ mod kt mod swift mod go mod dart -# The C++ package over moq-ffi. +# The C and C++ packages over moq-ffi. +mod c mod cpp # OBS Studio plugin (C++). See doc/bin/obs.md. mod obs 'cpp/obs' diff --git a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/audio/Audio.kt b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/audio/Audio.kt index ac9e6d37e16..cc279e5b7a8 100644 --- a/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/audio/Audio.kt +++ b/kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/audio/Audio.kt @@ -17,7 +17,7 @@ typealias Frame = uniffi.moq.MoqAudioFrame /** One encoded payload from an [Encoder]. */ typealias Packet = uniffi.moq.MoqAudioPacket /** The packets an [Encoder] drains on finish and the padded samples to discard. */ -typealias EncoderFinish = uniffi.moq.MoqAudioEncoderFinish +typealias EncoderFinish = uniffi.moq.MoqAudioEncoderFinished /** The audio codec: `Codec.opus()` or `Codec.aac()`. */ typealias Codec = uniffi.moq.MoqAudioCodec /** How the encoder trades latency for compression; `null` is `BALANCED`. */ diff --git a/py/moq-rs/moq/audio.py b/py/moq-rs/moq/audio.py index 52ea28b4ed1..aa7403bb06b 100644 --- a/py/moq-rs/moq/audio.py +++ b/py/moq-rs/moq/audio.py @@ -9,7 +9,7 @@ MoqAudioConsumer, MoqAudioDecoderOutput, MoqAudioEncoder, - MoqAudioEncoderFinish, + MoqAudioEncoderFinished, MoqAudioEncoderOutput, MoqAudioFrame, MoqAudioPacket, @@ -29,7 +29,7 @@ Packet = MoqAudioPacket """One encoded payload from an :class:`Encoder`.""" -EncoderFinish = MoqAudioEncoderFinish +EncoderFinish = MoqAudioEncoderFinished """The packets :meth:`Encoder.finish` drains and the padded samples to discard.""" diff --git a/quest/m1/c/README.md b/quest/m1/c/README.md index ed703acd670..9ccb853009c 100644 --- a/quest/m1/c/README.md +++ b/quest/m1/c/README.md @@ -52,5 +52,4 @@ That change owns its C tests and `MIGRATE.md` entries; other API reshaping stays ## Required -- [moq-c package](/quest/m1/c/package.md) - the generated header ships as `moq-c` 0.8.0 with `moq::c`, pkg-config, and a release workflow - [C consumers](/quest/m1/c/consumers.md) - the C interop client and `doc/lib/c` samples move onto the generated API diff --git a/quest/m1/c/consumers.md b/quest/m1/c/consumers.md index 79ec9006599..b2e7ada5398 100644 --- a/quest/m1/c/consumers.md +++ b/quest/m1/c/consumers.md @@ -12,7 +12,3 @@ build. Nothing in the repository calls the hand-written ABI any more. the callback and dispatcher shape end to end. - Rewrite `doc/lib/c` around the generated API, including a sample that runs callbacks on the host's own loop through `moq_set_dispatcher`. - -## Required - -- [moq-c package](/quest/m1/c/package.md) - the package these consumers link diff --git a/quest/m1/c/package.md b/quest/m1/c/package.md deleted file mode 100644 index f263341e818..00000000000 --- a/quest/m1/c/package.md +++ /dev/null @@ -1,33 +0,0 @@ -# [M] moq-c package from the generated C - -## Goal - -The generated C ships as the `moq-c` package, version 0.8.0: a release archive -with the header, the moq-ffi staticlib, a CMake config exporting `moq::c`, and -`moq-c.pc`, built the same way `cpp/moq` builds the C++ package. It replaces the -hand-written `rs/moq-c` crate's artifacts (renamed from libmoq by #4288) -under the same names. - -## Plan - -- Mirror `cpp/moq`: CMake runs `cargo build -p moq-ffi`, then the generator's C - backend, and installs the result; a probe test compiles and links from the - installed package through both `find_package(moq-c)` and pkg-config. -- The installed CMake config honors `CMAKE_INSTALL_LIBDIR`, so a `lib64` - distro gets a working `find_package`; the hand-written crate's config - hardcodes `/lib`. -- `doc/index.md` says the C library ships static and shared; say what the - package actually ships. -- A release workflow and nightly/CI jobs follow the C++ package's, on the same - generator pin; `v0.11.0-kixelated.4+v0.32.2` already carries the C backend - (`--lang c`). -- Ship a `MIGRATE.md` with the package, since it is what C users install. - Carry over `rs/moq-c/MIGRATE.md`'s history and add an `## Unreleased` - entry mapping the hand-written ABI to the generated one. -- Ship a hand-written `moq_shutdown` that calls `moq_ffi_shutdown`, then - `moq_shutdown_dispatcher`. -- moq-c and moq-cpp each embed the moq-ffi staticlib, so one program can't link - both; say so in the docs. -- The Cross-Package Sync rows that name the hand-written `rs/moq-c` (as the - target of `rs/moq-ffi` changes, and its `moq.h` C ABI) point at the - generated package instead; update `AGENTS.md` in this quest. diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 2d09c4a2fe3..03d841c0b9b 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -5,6 +5,11 @@ These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in it ## Unreleased +### `MoqAudioEncoderFinish` is `MoqAudioEncoderFinished` (#PR) + +The record `MoqAudioEncoder::finish` returns is renamed, so its C name no longer collides with the method's. +The wrappers' `EncoderFinish` aliases are unchanged. + ### Track demand reports the wanted group (#5930) `MoqTrackDemand::is_used`, `used`, and `unused` are gone. diff --git a/rs/moq-ffi/src/audio.rs b/rs/moq-ffi/src/audio.rs index d91929fba5d..1a44b44d85b 100644 --- a/rs/moq-ffi/src/audio.rs +++ b/rs/moq-ffi/src/audio.rs @@ -210,7 +210,7 @@ pub struct MoqAudioPacket { /// The terminal packets and decoded padding emitted when an audio encoder finishes. #[derive(uniffi::Record)] -pub struct MoqAudioEncoderFinish { +pub struct MoqAudioEncoderFinished { /// Packets emitted while draining the encoder. pub packets: Vec, /// Padded samples to discard from the decoded tail. @@ -302,10 +302,10 @@ impl MoqAudioEncoder { } /// Drain the encoder, optionally padding one final partial frame. - pub fn finish(&self, pcm: Vec) -> Result { + pub fn finish(&self, pcm: Vec) -> Result { let encoder = self.inner.lock().unwrap().take().ok_or(MoqError::Closed)?; let finish = encoder.finish(&pcm)?; - Ok(MoqAudioEncoderFinish { + Ok(MoqAudioEncoderFinished { discard_padding: finish.discard_padding() as u64, packets: audio_packets(finish.into_packets()), }) diff --git a/sh/c/check.sh b/sh/c/check.sh new file mode 100755 index 00000000000..44c521be83b --- /dev/null +++ b/sh/c/check.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Build and install c/moq, then build the C probe against the installed package through CMake +# and run it. Outside Windows, also builds and runs it through pkg-config. A source build +# installs to lib64/, as some distros do, so find_package and pkg-config are proven to honor +# CMAKE_INSTALL_LIBDIR; archives use lib/. CC picks the compiler on Unix; Windows uses MSVC. +set -euo pipefail + +ffi_dir="" +archive="" +if [[ $# -gt 0 ]]; then + if [[ $# != 2 || ("$1" != --ffi-dir && "$1" != --archive) ]]; then + echo "Usage: $0 [--ffi-dir DIR | --archive FILE]" >&2 + exit 1 + fi + if [[ "$1" == --archive ]]; then + archive="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")" + [[ -f "$archive" ]] || { + echo "Archive not found: $archive" >&2 + exit 1 + } + else + ffi_dir="$(cd "$2" && pwd)" + fi +fi + +cd "$(dirname "${BASH_SOURCE[0]}")/../../c" + +# Single-config generators build Debug. Visual Studio ignores CMAKE_BUILD_TYPE and gets +# --config Release, since the Rust staticlib links the release CRT (/MD) either way. +build=moq/build +libdir=lib64 +if [[ -n "$ffi_dir" ]]; then build=moq/build/prebuilt; fi +if [[ -n "$archive" ]]; then + build=moq/build/archive + libdir=lib + version=$(tr -d '[:space:]' &2 + exit 1 + fi + prefix="$PWD/$build/package/$name" + rm -rf "$build/package" + mkdir -p "$build/package" + (cd "$build/package" && cmake -E tar xf "$archive") + if [[ ! -f "$prefix/lib/cmake/moq-c/moq-c-config.cmake" ]]; then + echo "Missing moq-c package in $archive" >&2 + exit 1 + fi +else + prefix="$PWD/$build/prefix" + cmake -S moq -B "$build/package" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_LIBDIR="$libdir" -DMOQ_FFI_DIR="$ffi_dir" + cmake --build "$build/package" --config Release + rm -rf "$prefix" + cmake --install "$build/package" --config Release --prefix "$prefix" +fi + +run() { + if [[ -x "$1/probe" ]]; then "$1/probe"; else "$1/Release/probe.exe"; fi +} +cmake -S moq/test -B "$build/test" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PREFIX_PATH="$prefix" +cmake --build "$build/test" --config Release +run "$build/test" + +# The rest builds with `cc`, which on Windows is MinGW GCC and can't link the MSVC-built +# package. MSVC consumers use the CMake package. +if [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* ]]; then + exit 0 +fi + +if command -v pkg-config >/dev/null 2>&1; then + export PKG_CONFIG_PATH="$prefix/$libdir/pkgconfig" + # Assigned first: set -e ignores a failing substitution inside a command's arguments. + flags=$(pkg-config --cflags --libs moq-c) + mkdir -p "$build/pkg-config" + # shellcheck disable=SC2086 # flags is a flag list. + "${CC:-cc}" -std=c11 -Wall -Wextra -Werror -pedantic-errors -o "$build/pkg-config/probe" moq/test/probe.c $flags + run "$build/pkg-config" +fi diff --git a/sh/dispatch-query.test.py b/sh/dispatch-query.test.py index e0cc3515e06..55aea7cbb5b 100644 --- a/sh/dispatch-query.test.py +++ b/sh/dispatch-query.test.py @@ -168,7 +168,7 @@ def test_platform_skips_quest_and_js(self): def test_platform_rust_change_rebuilds_external_consumers(self): self.change("rs/moq-net/src/lib.rs") result = self.platform() - for suite in ("rs", "swift", "cpp", "obs_compile", "needed"): + for suite in ("rs", "swift", "c", "cpp", "obs_compile", "needed"): self.assertEqual(result[suite], "true", suite) self.assertEqual(result["moq_sh"], "false") diff --git a/sh/dispatch.sh b/sh/dispatch.sh index ba682b78fc7..af1d4f5ebcb 100755 --- a/sh/dispatch.sh +++ b/sh/dispatch.sh @@ -84,6 +84,8 @@ declare -A scope=( [swift]='^(swift/|sh/swift/|rs/moq-ffi/|doc/lib/swift/|doc/lib/samples\.sh$)' [go]='^(go/|sh/go/|rs/moq-ffi/|doc/lib/go/|doc/lib/samples\.sh$)' [dart]='^(dart/|sh/dart/|rs/moq-ffi/|doc/lib/dart/|doc/lib/samples\.sh$)' + # The C package links moq-c's native-libs lists, and the C++ package's FFI pin. + [c]='^(c/|sh/c/|rs/moq-ffi/|rs/moq-c/native-libs/|cpp/moq/FFI_VERSION$)' # The package links moq-c's native-libs lists, and compiles its doc samples. [cpp]='^(cpp/moq/|cpp/justfile$|sh/cpp/|rs/moq-ffi/|rs/moq-c/native-libs/|doc/lib/cpp/|doc/lib/samples\.sh$)' # The plugin and its tests call through the bindings generated from cpp/moq @@ -114,11 +116,11 @@ if [[ "$action" == ci-platform ]]; then if grep -qE '^(\.cargo/|\.github/|[^/]+/justfile$|justfile$)' "$changed"; then shared=1 fi - for module in rs swift cpp obs_compile moq_sh; do + for module in rs swift c cpp obs_compile moq_sh; do enabled=false pattern=${scope[$module]} case "$module" in - swift | cpp | obs_compile) pattern+="|${scope[rs]}" ;; + swift | c | cpp | obs_compile) pattern+="|${scope[rs]}" ;; esac if [[ -n "$shared" ]] || grep -qE "$pattern" "$changed"; then enabled=true @@ -148,6 +150,7 @@ declare -A tools=( [swift]='' [go]='cargo go uniffi-bindgen-go' [dart]='cargo dart uniffi_bindgen_dart' + [c]='cargo cmake cc c++ pkg-config uniffi-bindgen-cpp' [cpp]='cargo cmake c++ uniffi-bindgen-cpp' [obs_compile]='cargo cmake pkg-config uniffi-bindgen-cpp' [obs]='clang-format cmake gersemi jq' @@ -169,13 +172,13 @@ verb=${action#ci-} # build cache only when one is selected. JS checks build @moq/wasm for its # consumers, and the bindings build moq-ffi. declare -A compiles=( - [check]='rs bench js py kt go dart cpp obs_compile' + [check]='rs bench js py kt go dart c cpp obs_compile' [fix]='rs' [test]='rs py' ) case "$action" in - check | ci-check) modules=(js workers moq_sh drafts rs bench quest drill py kt swift go dart cpp obs_compile obs flake ts_paths markdown shell toml nix justfile gh) ;; + check | ci-check) modules=(js workers moq_sh drafts rs bench quest drill py kt swift go dart c cpp obs_compile obs flake ts_paths markdown shell toml nix justfile gh) ;; fix) modules=(js rs py dart obs markdown shell toml nix justfile) ;; ci-test) modules=(js rs py) ;; esac diff --git a/swift/Sources/Moq/Audio.swift b/swift/Sources/Moq/Audio.swift index ab276250038..974b7dc6db5 100644 --- a/swift/Sources/Moq/Audio.swift +++ b/swift/Sources/Moq/Audio.swift @@ -13,7 +13,7 @@ public enum Audio { /// One encoded payload from an `Encoder`. public typealias Packet = MoqAudioPacket /// The packets an `Encoder` drains on finish and the padded samples to discard. - public typealias EncoderFinish = MoqAudioEncoderFinish + public typealias EncoderFinish = MoqAudioEncoderFinished /// Selects the audio encoder codec. Build one with `Codec.opus()` or `Codec.aac()`. public typealias Codec = AudioCodec /// How the encoder trades latency for compression; `nil` is `.balanced`. From b8e5b49ec28c3f8b9272c3f272ec448464f33ba5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 23:06:38 -0700 Subject: [PATCH 03/25] docs(c): format MIGRATE.md Co-Authored-By: Claude Opus 5.5 --- c/moq/MIGRATE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/c/moq/MIGRATE.md b/c/moq/MIGRATE.md index 780f696142c..d7037e63b94 100644 --- a/c/moq/MIGRATE.md +++ b/c/moq/MIGRATE.md @@ -54,7 +54,6 @@ It embeds the same Rust runtime as the C++ package, `moq-cpp`, so a program link ## moq-c 0.7, the hand-written ABI - ### Demand reports the wanted group (#5930) `moq_track_info` gains `refresh` and `refresh_present`, which changes its ABI; a zeroed field leaves refresh off. From 61c8e0cebfb0d15b0974d205d19720df0701088f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 23:06:55 -0700 Subject: [PATCH 04/25] docs: link MIGRATE entries to #6052 Co-Authored-By: Claude Opus 5.5 --- c/moq/MIGRATE.md | 2 +- cpp/moq/MIGRATE.md | 2 +- rs/moq-ffi/MIGRATE.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/c/moq/MIGRATE.md b/c/moq/MIGRATE.md index d7037e63b94..510872e99a9 100644 --- a/c/moq/MIGRATE.md +++ b/c/moq/MIGRATE.md @@ -4,7 +4,7 @@ How to update your code for each breaking change, newest first. ## Unreleased -### The C API is generated from moq-ffi (0.8.0) +### The C API is generated from moq-ffi (#6052) moq-c 0.8.0 replaces the hand-written ABI with one generated from moq-ffi, the crate every other binding wraps, so a function here means what its method means in Python, Go, or C++. Every call changes shape once; the names are moq-ffi's in snake case, and `` documents each one. diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md index efe1e4b9561..b3ab63b6fcf 100644 --- a/cpp/moq/MIGRATE.md +++ b/cpp/moq/MIGRATE.md @@ -4,7 +4,7 @@ How to update your code for each breaking change, newest first. ## Unreleased -### `AudioEncoderFinish` is `AudioEncoderFinished` (#PR) +### `AudioEncoderFinish` is `AudioEncoderFinished` (#6052) `moq::AudioEncoder::finish` returns the renamed record; its fields are unchanged. diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 03d841c0b9b..1d0786af000 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -5,7 +5,7 @@ These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in it ## Unreleased -### `MoqAudioEncoderFinish` is `MoqAudioEncoderFinished` (#PR) +### `MoqAudioEncoderFinish` is `MoqAudioEncoderFinished` (#6052) The record `MoqAudioEncoder::finish` returns is renamed, so its C name no longer collides with the method's. The wrappers' `EncoderFinish` aliases are unchanged. From 3b526ffd15df67238ac6444d95c165a3337859db Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 23:27:33 -0700 Subject: [PATCH 05/25] fix(c): find the lib64 probe package on Debian CMake and ask cargo for its target dir Debian-family CMake never searches lib64/ under a prefix, so the clang job's probe could not find the lib64 install through CMAKE_PREFIX_PATH. Name its config directory instead; archives (lib/) still use the prefix. c/moq and cpp/moq now resolve the cargo target directory through `cargo metadata`, so a relative CARGO_TARGET_DIR or a configured build.target-dir points bindgen, link, and install at the library cargo actually built. Co-Authored-By: Claude Opus 5.5 --- c/moq/CMakeLists.txt | 14 +++++++++----- cpp/moq/CMakeLists.txt | 14 +++++++++----- sh/c/check.sh | 6 +++++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/c/moq/CMakeLists.txt b/c/moq/CMakeLists.txt index ccc4367fb5f..6462a9bc586 100644 --- a/c/moq/CMakeLists.txt +++ b/c/moq/CMakeLists.txt @@ -47,11 +47,15 @@ else() set(_profile release) set(_cargo_flags --release) endif() - if(DEFINED ENV{CARGO_TARGET_DIR}) - set(_cargo_target "$ENV{CARGO_TARGET_DIR}") - else() - set(_cargo_target "${_workspace}/target") - endif() + # Ask cargo, so CARGO_TARGET_DIR (even relative) and a configured build.target-dir both land + # where the build below puts the library. + execute_process( + COMMAND cargo metadata --locked --no-deps --format-version 1 + WORKING_DIRECTORY "${_workspace}" + OUTPUT_VARIABLE _cargo_metadata + COMMAND_ERROR_IS_FATAL ANY + ) + string(JSON _cargo_target GET "${_cargo_metadata}" target_directory) endif() if(WIN32) diff --git a/cpp/moq/CMakeLists.txt b/cpp/moq/CMakeLists.txt index 5e0af902fb7..794c2f13736 100644 --- a/cpp/moq/CMakeLists.txt +++ b/cpp/moq/CMakeLists.txt @@ -44,11 +44,15 @@ else() set(_profile release) set(_cargo_flags --release) endif() - if(DEFINED ENV{CARGO_TARGET_DIR}) - set(_cargo_target "$ENV{CARGO_TARGET_DIR}") - else() - set(_cargo_target "${_workspace}/target") - endif() + # Ask cargo, so CARGO_TARGET_DIR (even relative) and a configured build.target-dir both land + # where the build below puts the library. + execute_process( + COMMAND cargo metadata --locked --no-deps --format-version 1 + WORKING_DIRECTORY "${_workspace}" + OUTPUT_VARIABLE _cargo_metadata + COMMAND_ERROR_IS_FATAL ANY + ) + string(JSON _cargo_target GET "${_cargo_metadata}" target_directory) endif() if(MOQ_FFI_DIR) diff --git a/sh/c/check.sh b/sh/c/check.sh index 44c521be83b..608cf8d9281 100755 --- a/sh/c/check.sh +++ b/sh/c/check.sh @@ -61,7 +61,11 @@ fi run() { if [[ -x "$1/probe" ]]; then "$1/probe"; else "$1/Release/probe.exe"; fi } -cmake -S moq/test -B "$build/test" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PREFIX_PATH="$prefix" +# Debian-family CMake never searches lib64/ under a prefix, so a lib64 install is found by its +# config directory, as a Debian user of one would have to. +find=(-DCMAKE_PREFIX_PATH="$prefix") +[[ "$libdir" == lib ]] || find=(-Dmoq-c_DIR="$prefix/$libdir/cmake/moq-c") +cmake -S moq/test -B "$build/test" -DCMAKE_BUILD_TYPE=Debug "${find[@]}" cmake --build "$build/test" --config Release run "$build/test" From b0cffbb1e27d699daa9cb64982bea16580601f90 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:13:51 -0700 Subject: [PATCH 06/25] fix(c): say callbacks may run during moq_shutdown, never after moq_shutdown drains the dispatcher, so a callback already in flight can still run while it does; the header promised pending calls never call back. It now says none run once it returns, which the probe checks with a read whose frame lands just before shutdown and handles freed after. Co-Authored-By: Claude Opus 5.5 --- c/moq/MIGRATE.md | 2 +- c/moq/include/moq/moq.h | 4 +++- c/moq/test/probe.c | 37 +++++++++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/c/moq/MIGRATE.md b/c/moq/MIGRATE.md index 484af9b601e..3ae78e1cdae 100644 --- a/c/moq/MIGRATE.md +++ b/c/moq/MIGRATE.md @@ -21,7 +21,7 @@ Every call changes shape once; the names are moq-ffi's in snake case, and `done, 5000)) { @@ -357,8 +365,33 @@ int main(void) { moq_broadcast_producer_free(broadcast); moq_origin_producer_free(origin); + /* A read whose frame arrives just before shutdown has its callback in flight, so it may run + * while moq_shutdown drains the dispatcher; none may run once it returns, even as the + * handles are freed afterward. */ + moq_origin_producer *late_origin = moq_origin_producer_new(&origin_config); + moq_broadcast_producer *late_broadcast = NULL; + ok(moq_origin_producer_create_broadcast(late_origin, "late", &late_broadcast), "late broadcast"); + moq_track_producer *late_track = NULL; + ok(moq_broadcast_producer_publish_track(late_broadcast, "data", &info, &late_track), "late track"); + moq_track_consumer *late = NULL; + ok(moq_track_producer_consume(late_track, NULL, &late), "late consume"); + result in_flight = {0}; + task = moq_track_consumer_read_frame(late, on_late, &in_flight); + ok(moq_track_producer_write_frame(late_track, &frame), "late write"); + /* Stop the runtime and the dispatcher before the process tears down. */ moq_shutdown(); + shut_down = true; + + if (in_flight.done) { + CHECK(!in_flight.error); + moq_frame_free(in_flight.value); + } + moq_task_free(task); + moq_track_consumer_free(late); + moq_track_producer_free(late_track); + moq_broadcast_producer_free(late_broadcast); + moq_origin_producer_free(late_origin); printf("probe: ok\n"); return 0; From c430b76c20cb13ed6618dffa4cfd2a07523e07e9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:15:03 -0700 Subject: [PATCH 07/25] fix(c): run check directly from the module's default recipe `just check` ran from c/, where c/justfile's justfile_directory() is c/, so a bare `just c` called the missing c/sh/c/check.sh. Co-Authored-By: Claude Opus 5.5 --- c/justfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/c/justfile b/c/justfile index 136ac0056ea..bd9ee813445 100644 --- a/c/justfile +++ b/c/justfile @@ -5,8 +5,7 @@ set working-directory := '.' -default: - just check +default: check # Build the package, or check an archive, and run its installed consumers. [no-cd] From 0d626a011d7a2f43402351b7f75c86e277fdb889 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:23:43 -0700 Subject: [PATCH 08/25] fix(c,cpp): share one moq-ffi cargo target per build tree c/moq and cpp/moq each declared the cargo outputs as byproducts of their own render target, so a project adding both failed under Ninja with multiple rules for libmoq_ffi.a. Whichever package configures first now creates `moq_ffi_cargo`, and both render targets depend on it. Co-Authored-By: Claude Opus 5.5 --- c/moq/CMakeLists.txt | 24 ++++++++++++++++++------ cpp/moq/CMakeLists.txt | 24 ++++++++++++++++++------ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/c/moq/CMakeLists.txt b/c/moq/CMakeLists.txt index 6462a9bc586..3fd70b3be2b 100644 --- a/c/moq/CMakeLists.txt +++ b/c/moq/CMakeLists.txt @@ -74,12 +74,22 @@ if(MOQ_FFI_DIR) message(FATAL_ERROR "Missing prebuilt FFI input: ${_input}") endif() endforeach() - set(_build_ffi "") + set(_cargo_ffi "") else() # The package ships the OpenH264 software fallback, like the C++ package and the prebuilt # input do. - set(_build_ffi COMMAND cargo build --locked --package moq-ffi --features openh264 ${_cargo_flags}) - set(_ffi_byproducts "${MOQ_FFI_LIB}" "${_metadata}") + # One cargo target per build tree, so a project adding both c/moq and cpp/moq (which build + # the same moq-ffi) has one rule for its outputs, as Ninja requires. + if(NOT TARGET moq_ffi_cargo) + add_custom_target(moq_ffi_cargo + COMMAND cargo build --locked --package moq-ffi --features openh264 ${_cargo_flags} + WORKING_DIRECTORY "${_workspace}" + BYPRODUCTS "${MOQ_FFI_LIB}" "${_metadata}" + COMMENT "Building moq-ffi" + VERBATIM + ) + endif() + set(_cargo_ffi moq_ffi_cargo) endif() find_program(UNIFFI_BINDGEN_CPP uniffi-bindgen-cpp REQUIRED) @@ -95,16 +105,18 @@ set(_render "${CMAKE_CURRENT_BINARY_DIR}/render") list(TRANSFORM _generated_files PREPEND "${_render}/" OUTPUT_VARIABLE _render_paths) # Named apart from cpp/moq's `moq_ffi`, so both packages can build in one tree. add_custom_target(moq_c_ffi ALL - ${_build_ffi} COMMAND ${CMAKE_COMMAND} -E rm -rf "${_render}" COMMAND "${UNIFFI_BINDGEN_CPP}" --lang c --library "${_metadata}" --out-dir "${_render}" COMMAND ${CMAKE_COMMAND} -E make_directory "${_generated}" COMMAND ${CMAKE_COMMAND} -E copy_if_different ${_render_paths} "${_generated}" WORKING_DIRECTORY "${_workspace}" - BYPRODUCTS ${_ffi_byproducts} ${_generated_paths} - COMMENT "Preparing moq-ffi and rendering its C bindings" + BYPRODUCTS ${_generated_paths} + COMMENT "Rendering the moq-ffi C bindings" VERBATIM ) +if(_cargo_ffi) + add_dependencies(moq_c_ffi ${_cargo_ffi}) +endif() # System libraries the staticlib needs from an external linker. moq-ffi and moq-c share one # dependency tree, so they share rs/moq-c/native-libs/, which says how to regenerate it. diff --git a/cpp/moq/CMakeLists.txt b/cpp/moq/CMakeLists.txt index 794c2f13736..11623900800 100644 --- a/cpp/moq/CMakeLists.txt +++ b/cpp/moq/CMakeLists.txt @@ -75,12 +75,22 @@ if(MOQ_FFI_DIR) message(FATAL_ERROR "Missing prebuilt FFI input: ${_input}") endif() endforeach() - set(_build_ffi "") + set(_cargo_ffi "") else() # The OpenH264 software fallback is opt-in in Rust; the C++ package ships it, like the # prebuilt input does. - set(_build_ffi COMMAND cargo build --locked --package moq-ffi --features openh264 ${_cargo_flags}) - set(_ffi_byproducts "${MOQ_FFI_LIB}" "${_metadata}") + # One cargo target per build tree, so a project adding both c/moq and cpp/moq (which build + # the same moq-ffi) has one rule for its outputs, as Ninja requires. + if(NOT TARGET moq_ffi_cargo) + add_custom_target(moq_ffi_cargo + COMMAND cargo build --locked --package moq-ffi --features openh264 ${_cargo_flags} + WORKING_DIRECTORY "${_workspace}" + BYPRODUCTS "${MOQ_FFI_LIB}" "${_metadata}" + COMMENT "Building moq-ffi" + VERBATIM + ) + endif() + set(_cargo_ffi moq_ffi_cargo) endif() find_program(UNIFFI_BINDGEN_CPP uniffi-bindgen-cpp REQUIRED) @@ -96,17 +106,19 @@ list(TRANSFORM _generated_files PREPEND "${_generated}/" OUTPUT_VARIABLE _genera set(_render "${CMAKE_CURRENT_BINARY_DIR}/render") list(TRANSFORM _generated_files PREPEND "${_render}/" OUTPUT_VARIABLE _render_paths) add_custom_target(moq_ffi ALL - ${_build_ffi} COMMAND ${CMAKE_COMMAND} -E rm -rf "${_render}" COMMAND "${UNIFFI_BINDGEN_CPP}" --library "${_metadata}" --config "${CMAKE_CURRENT_SOURCE_DIR}/uniffi.toml" --out-dir "${_render}" COMMAND ${CMAKE_COMMAND} -E make_directory "${_generated}" COMMAND ${CMAKE_COMMAND} -E copy_if_different ${_render_paths} "${_generated}" WORKING_DIRECTORY "${_workspace}" - BYPRODUCTS ${_ffi_byproducts} ${_generated_paths} - COMMENT "Preparing moq-ffi and rendering its C++ bindings" + BYPRODUCTS ${_generated_paths} + COMMENT "Rendering the moq-ffi C++ bindings" VERBATIM ) +if(_cargo_ffi) + add_dependencies(moq_ffi ${_cargo_ffi}) +endif() # System libraries the staticlib needs from an external linker. moq-ffi and moq-c share one # dependency tree, so they share rs/moq-c/native-libs/, which says how to regenerate it. if(APPLE) From 1da8558eb7df6f85d337f28d82e7807f68008821 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:30:26 -0700 Subject: [PATCH 09/25] test(c): dry-run a Ninja build that adds both c/moq and cpp/moq The check fails with "multiple rules generate libmoq_ffi.a" without the shared cargo target, so it guards the fix. It only generates and dry-runs, so it adds seconds, and runs wherever Ninja is installed. Co-Authored-By: Claude Opus 5.5 --- c/moq/README.md | 2 +- c/moq/test/superbuild/CMakeLists.txt | 7 +++++++ sh/c/check.sh | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 c/moq/test/superbuild/CMakeLists.txt diff --git a/c/moq/README.md b/c/moq/README.md index 03bbae4c75e..e5b17b0756c 100644 --- a/c/moq/README.md +++ b/c/moq/README.md @@ -12,7 +12,7 @@ The C API for [rs/moq-ffi](../../rs/moq-ffi), generated by the C backend of `uni ## Checking -`just c check` builds the package, installs it to a `lib64` prefix, builds the probe against it through `find_package` and, outside Windows, through pkg-config, and runs each. `just c check --archive FILE` runs the same consumer checks against an extracted release archive. Release dry runs check every archive this way. +`just c check` builds the package, installs it to a `lib64` prefix, builds the probe against it through `find_package` and, outside Windows, through pkg-config, and runs each. With Ninja available, it also dry-runs a build that adds both `c/moq` and `cpp/moq`, which must share one cargo rule. `just c check --archive FILE` runs the same consumer checks against an extracted release archive. Release dry runs check every archive this way. The generator is the one the C++ package pins; see [cpp/moq](../../cpp/moq/README.md) to install it without Nix. `flake.nix` lists every place that names its tag. diff --git a/c/moq/test/superbuild/CMakeLists.txt b/c/moq/test/superbuild/CMakeLists.txt new file mode 100644 index 00000000000..d509260f333 --- /dev/null +++ b/c/moq/test/superbuild/CMakeLists.txt @@ -0,0 +1,7 @@ +# A project that adds both c/moq and cpp/moq from source. `just c check` generates it with Ninja +# and dry-runs the build, which fails if both packages claim the same cargo outputs. +cmake_minimum_required(VERSION 3.21) +project(moq-superbuild LANGUAGES C CXX) + +add_subdirectory(../.. c) +add_subdirectory(../../../../cpp/moq cpp) diff --git a/sh/c/check.sh b/sh/c/check.sh index 608cf8d9281..10a5d8d756f 100755 --- a/sh/c/check.sh +++ b/sh/c/check.sh @@ -58,6 +58,12 @@ else cmake --install "$build/package" --config Release --prefix "$prefix" fi +# Ninja refuses two rules for one output, so a dry run proves both packages share one cargo rule. +if [[ -z "$archive" && -z "$ffi_dir" ]] && command -v ninja >/dev/null 2>&1; then + cmake -G Ninja -S moq/test/superbuild -B "$build/superbuild" -DCMAKE_BUILD_TYPE=Debug + ninja -C "$build/superbuild" -n >/dev/null +fi + run() { if [[ -x "$1/probe" ]]; then "$1/probe"; else "$1/Release/probe.exe"; fi } From 01e5f457de1d144b129e1356d658b107122eb5a8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:38:00 -0700 Subject: [PATCH 10/25] ci(c): run the clang job on release PRs and nightly, like cpp.yml Main PRs are gated only by the merge queue, so the path-filtered clang job moves to release PRs plus a nightly call on main. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/c.yml | 10 +++++++--- .github/workflows/nightly.yml | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/c.yml b/.github/workflows/c.yml index b01fd717951..7ded75e813c 100644 --- a/.github/workflows/c.yml +++ b/.github/workflows/c.yml @@ -12,10 +12,14 @@ permissions: contents: read on: + workflow_call: pull_request: - # `closed` is here only so merging/closing a PR cancels its in-flight run - # via the concurrency group below; the job itself is skipped on close. - types: [opened, synchronize, reopened, closed] + # Main PRs skip it; nightly.yml runs it on main. `edited` catches a main PR + # retargeted to release. `closed` is here only so merging/closing a PR + # cancels its in-flight run via the concurrency group below; the job itself + # is skipped on close. + branches: [release] + types: [opened, synchronize, reopened, edited, closed] paths: - "c/**" - "rs/moq-ffi/**" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 1b53ebc737e..21457c9e393 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -28,7 +28,7 @@ name: Nightly # release PRs on them. # # Main PRs run no cloud CI beyond the merge queue's Check and Test, so every -# path-filtered gate (Android, C++, WASM, X11, the audio replays, platform.yml +# path-filtered gate (Android, C, C++, WASM, X11, the audio replays, platform.yml # and the release dry-runs) runs here on main too, and on release PRs. # # A failure here lands on main rather than being caught in review, which is the @@ -54,6 +54,9 @@ jobs: android: uses: ./.github/workflows/android.yml + c: + uses: ./.github/workflows/c.yml + cpp: uses: ./.github/workflows/cpp.yml From 5f571d9f8104bcce756912dc13f51470735e6ea2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:38:00 -0700 Subject: [PATCH 11/25] fix(c,cpp): refuse CARGO_BUILD_TARGET for source builds A build target moves the library under /, which cargo metadata does not report, so bindgen would miss it. Both packages build moq-ffi for the host only, as their build.sh already insists. Co-Authored-By: Claude Opus 5.5 --- c/moq/CMakeLists.txt | 5 +++++ cpp/moq/CMakeLists.txt | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/c/moq/CMakeLists.txt b/c/moq/CMakeLists.txt index 3fd70b3be2b..42fd60c140d 100644 --- a/c/moq/CMakeLists.txt +++ b/c/moq/CMakeLists.txt @@ -47,6 +47,11 @@ else() set(_profile release) set(_cargo_flags --release) endif() + # A build target, even the host's, moves the library under /, which cargo metadata + # does not report. A `[build] target` in a cargo config fails later, at the missing library. + if(DEFINED ENV{CARGO_BUILD_TARGET}) + message(FATAL_ERROR "Unsupported CARGO_BUILD_TARGET: moq-ffi builds for the host here; unset it") + endif() # Ask cargo, so CARGO_TARGET_DIR (even relative) and a configured build.target-dir both land # where the build below puts the library. execute_process( diff --git a/cpp/moq/CMakeLists.txt b/cpp/moq/CMakeLists.txt index 11623900800..32b3f2da641 100644 --- a/cpp/moq/CMakeLists.txt +++ b/cpp/moq/CMakeLists.txt @@ -44,6 +44,11 @@ else() set(_profile release) set(_cargo_flags --release) endif() + # A build target, even the host's, moves the library under /, which cargo metadata + # does not report. A `[build] target` in a cargo config fails later, at the missing library. + if(DEFINED ENV{CARGO_BUILD_TARGET}) + message(FATAL_ERROR "Unsupported CARGO_BUILD_TARGET: moq-ffi builds for the host here; unset it") + endif() # Ask cargo, so CARGO_TARGET_DIR (even relative) and a configured build.target-dir both land # where the build below puts the library. execute_process( From 86df27fda5620e95dc8fa90cf7857674cd194a38 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:38:47 -0700 Subject: [PATCH 12/25] test(c): accept a read cancelled by shutdown in the probe A drained poll after the runtime stops resolves Cancelled before it sees the frame, which the moq_shutdown contract allows. Co-Authored-By: Claude Opus 5.5 --- c/moq/test/probe.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/c/moq/test/probe.c b/c/moq/test/probe.c index 3bbd8621726..6f073eec3c9 100644 --- a/c/moq/test/probe.c +++ b/c/moq/test/probe.c @@ -383,8 +383,11 @@ int main(void) { moq_shutdown(); shut_down = true; - if (in_flight.done) { - CHECK(!in_flight.error); + /* Shutdown may have cancelled the read before it saw the frame. */ + if (in_flight.done && in_flight.error) { + CHECK(in_flight.error->tag == MOQ_ERROR_CANCELLED); + moq_error_free(in_flight.error); + } else if (in_flight.done) { moq_frame_free(in_flight.value); } moq_task_free(task); From ec97c1bcf0ec0520a5462386d016cd3ccceb39ed Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:50:12 -0700 Subject: [PATCH 13/25] fix(c,cpp): take moq-ffi's paths from cargo and keep installs apart c/moq and cpp/moq now include one cpp/moq/cmake/moq-ffi.cmake for the native input: the prebuilt manifest checks, the native-libs list, and a cargo build that reads the moq_ffi artifact's filenames from cargo's JSON messages and hard-links them into the top-level build tree. No cargo setting (CARGO_TARGET_DIR, build.target-dir, build.target) can move the libraries from where the packages look, so the CARGO_BUILD_TARGET refusal and the cargo metadata lookup are gone. moq-c installs its moq-ffi archive as libmoq_c_ffi.a (moq_c_ffi.lib), so moq-cpp, released at its own moq-ffi pin, can install to the same prefix without overwriting it. Co-Authored-By: Claude Opus 5.5 --- c/moq/CMakeLists.txt | 126 ++---------------------------- c/moq/MIGRATE.md | 4 +- c/moq/README.md | 2 +- c/moq/cmake/moq-c.pc.in | 2 +- cpp/moq/CMakeLists.txt | 122 +---------------------------- cpp/moq/cmake/moq-ffi.cmake | 150 ++++++++++++++++++++++++++++++++++++ 6 files changed, 166 insertions(+), 240 deletions(-) create mode 100644 cpp/moq/cmake/moq-ffi.cmake diff --git a/c/moq/CMakeLists.txt b/c/moq/CMakeLists.txt index 42fd60c140d..5c98c143498 100644 --- a/c/moq/CMakeLists.txt +++ b/c/moq/CMakeLists.txt @@ -10,92 +10,7 @@ endif() project(moq-c VERSION ${_version} LANGUAGES C CXX) set(_workspace "${CMAKE_CURRENT_SOURCE_DIR}/../..") -set(MOQ_FFI_DIR "" CACHE PATH "Extracted default-feature moq-ffi native package") -if(MOQ_FFI_DIR) - get_filename_component(MOQ_FFI_DIR "${MOQ_FFI_DIR}" ABSOLUTE) - file(READ "${MOQ_FFI_DIR}/manifest.json" _manifest) - foreach(_field version target features profile source) - string(JSON _input_${_field} GET "${_manifest}" ${_field}) - endforeach() - if(NOT _input_features STREQUAL "default" OR NOT _input_profile STREQUAL "release") - message(FATAL_ERROR "Prebuilt moq-ffi requires default features and release profile") - endif() - if(NOT _input_version MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+([-+][0-9A-Za-z.+-]+)?$") - message(FATAL_ERROR "Malformed prebuilt moq-ffi version: ${_input_version}") - endif() - if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") - set(_ffi_target aarch64-unknown-linux-gnu) - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") - set(_ffi_target x86_64-unknown-linux-gnu) - endif() - elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") - set(_ffi_target aarch64-apple-darwin) - elseif(MSVC AND CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|x86_64)$") - set(_ffi_target x86_64-pc-windows-msvc) - endif() - if(NOT DEFINED _ffi_target OR NOT _ffi_target STREQUAL _input_target) - message(FATAL_ERROR "Prebuilt moq-ffi target ${_input_target} does not match this host") - endif() - set(_cargo_target "${MOQ_FFI_DIR}/lib") - set(_profile "") -else() - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set(_profile debug) - set(_cargo_flags "") - else() - set(_profile release) - set(_cargo_flags --release) - endif() - # A build target, even the host's, moves the library under /, which cargo metadata - # does not report. A `[build] target` in a cargo config fails later, at the missing library. - if(DEFINED ENV{CARGO_BUILD_TARGET}) - message(FATAL_ERROR "Unsupported CARGO_BUILD_TARGET: moq-ffi builds for the host here; unset it") - endif() - # Ask cargo, so CARGO_TARGET_DIR (even relative) and a configured build.target-dir both land - # where the build below puts the library. - execute_process( - COMMAND cargo metadata --locked --no-deps --format-version 1 - WORKING_DIRECTORY "${_workspace}" - OUTPUT_VARIABLE _cargo_metadata - COMMAND_ERROR_IS_FATAL ANY - ) - string(JSON _cargo_target GET "${_cargo_metadata}" target_directory) -endif() - -if(WIN32) - set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/moq_ffi.lib") - set(_metadata "${_cargo_target}/${_profile}/moq_ffi.dll") -elseif(APPLE) - set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/libmoq_ffi.a") - set(_metadata "${_cargo_target}/${_profile}/libmoq_ffi.dylib") -else() - set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/libmoq_ffi.a") - set(_metadata "${_cargo_target}/${_profile}/libmoq_ffi.so") -endif() -if(MOQ_FFI_DIR) - foreach(_input "${MOQ_FFI_LIB}" "${_metadata}" "${MOQ_FFI_DIR}/native-libs.txt") - if(NOT EXISTS "${_input}") - message(FATAL_ERROR "Missing prebuilt FFI input: ${_input}") - endif() - endforeach() - set(_cargo_ffi "") -else() - # The package ships the OpenH264 software fallback, like the C++ package and the prebuilt - # input do. - # One cargo target per build tree, so a project adding both c/moq and cpp/moq (which build - # the same moq-ffi) has one rule for its outputs, as Ninja requires. - if(NOT TARGET moq_ffi_cargo) - add_custom_target(moq_ffi_cargo - COMMAND cargo build --locked --package moq-ffi --features openh264 ${_cargo_flags} - WORKING_DIRECTORY "${_workspace}" - BYPRODUCTS "${MOQ_FFI_LIB}" "${_metadata}" - COMMENT "Building moq-ffi" - VERBATIM - ) - endif() - set(_cargo_ffi moq_ffi_cargo) -endif() +include("${CMAKE_CURRENT_SOURCE_DIR}/../../cpp/moq/cmake/moq-ffi.cmake") find_program(UNIFFI_BINDGEN_CPP uniffi-bindgen-cpp REQUIRED) @@ -111,7 +26,7 @@ list(TRANSFORM _generated_files PREPEND "${_render}/" OUTPUT_VARIABLE _render_pa # Named apart from cpp/moq's `moq_ffi`, so both packages can build in one tree. add_custom_target(moq_c_ffi ALL COMMAND ${CMAKE_COMMAND} -E rm -rf "${_render}" - COMMAND "${UNIFFI_BINDGEN_CPP}" --lang c --library "${_metadata}" --out-dir "${_render}" + COMMAND "${UNIFFI_BINDGEN_CPP}" --lang c --library "${MOQ_FFI_METADATA}" --out-dir "${_render}" COMMAND ${CMAKE_COMMAND} -E make_directory "${_generated}" COMMAND ${CMAKE_COMMAND} -E copy_if_different ${_render_paths} "${_generated}" WORKING_DIRECTORY "${_workspace}" @@ -119,36 +34,9 @@ add_custom_target(moq_c_ffi ALL COMMENT "Rendering the moq-ffi C bindings" VERBATIM ) -if(_cargo_ffi) - add_dependencies(moq_c_ffi ${_cargo_ffi}) -endif() - -# System libraries the staticlib needs from an external linker. moq-ffi and moq-c share one -# dependency tree, so they share rs/moq-c/native-libs/, which says how to regenerate it. -if(APPLE) - set(_platform apple) -elseif(WIN32) - set(_platform windows) -else() - set(_platform linux) -endif() -if(MOQ_FFI_DIR) - set(_native_libs "${MOQ_FFI_DIR}/native-libs.txt") -else() - set(_native_libs "${_workspace}/rs/moq-c/native-libs/${_platform}.txt") +if(MOQ_FFI_CARGO) + add_dependencies(moq_c_ffi ${MOQ_FFI_CARGO}) endif() -file(STRINGS "${_native_libs}" _lines) -set(MOQ_NATIVE_LIBS "") -foreach(_line IN LISTS _lines) - string(STRIP "${_line}" _line) - if(_line STREQUAL "" OR _line MATCHES "^#") - continue() - elseif(_line MATCHES "^framework:(.+)$") - list(APPEND MOQ_NATIVE_LIBS "-framework ${CMAKE_MATCH_1}") - else() - list(APPEND MOQ_NATIVE_LIBS "${_line}") - endif() -endforeach() # What a C linker must add for the C++ implementation: the C++ compiler's runtime libraries # that the C compiler does not already link, such as libstdc++ or libc++. Empty on MSVC, whose @@ -202,11 +90,13 @@ if(PROJECT_IS_TOP_LEVEL) install(TARGETS moq-c ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(FILES include/moq/moq.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/moq) install(FILES "${_generated}/moq.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/moq/ffi) - install(FILES "${MOQ_FFI_LIB}" DESTINATION ${CMAKE_INSTALL_LIBDIR}) + # Renamed, so moq-cpp, which ships its own moq-ffi at its own pin, can install to the same + # prefix without either overwriting the other's archive. + set(MOQ_FFI_LIB_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}moq_c_ffi${CMAKE_STATIC_LIBRARY_SUFFIX}") + install(FILES "${MOQ_FFI_LIB}" DESTINATION ${CMAKE_INSTALL_LIBDIR} RENAME "${MOQ_FFI_LIB_NAME}") install(FILES MIGRATE.md DESTINATION ${CMAKE_INSTALL_DATADIR}/moq-c) # The config takes lists; pkg-config wants linker flags. - get_filename_component(MOQ_FFI_LIB_NAME "${MOQ_FFI_LIB}" NAME) set(MOQ_C_LIB_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}moq${CMAKE_STATIC_LIBRARY_SUFFIX}") set(MOQ_LIBS_PC "") foreach(_lib IN LISTS MOQ_NATIVE_LIBS MOQ_CXX_RUNTIME) diff --git a/c/moq/MIGRATE.md b/c/moq/MIGRATE.md index 3ae78e1cdae..679c3f1325c 100644 --- a/c/moq/MIGRATE.md +++ b/c/moq/MIGRATE.md @@ -12,7 +12,7 @@ Every call changes shape once; the names are moq-ffi's in snake case, and `` | `#include ` | -| `-lmoq` | `-lmoq -lmoq_ffi` plus the C++ runtime; take them from `pkg-config --libs moq-c` or `moq::c` | +| `-lmoq` | `-lmoq -lmoq_c_ffi` plus the C++ runtime; take them from `pkg-config --libs moq-c` or `moq::c` | | `int32_t` handles | opaque pointers, each released with its `moq__free` | | a negative return code, then `moq_error()` | a returned `moq_error *` (NULL on success) with `tag`, its fields, and `moq_error_message()`; free it with `moq_error_free` | | results written to `dst` | results written to the last argument, `out` | @@ -49,7 +49,7 @@ moq_task *task = moq_track_consumer_read_frame(track, on_frame, user_data); // Free it after the callback, or before to cancel the read. ``` -The archive holds the compiled C API (`libmoq.a` / `moq.lib`) and moq-ffi's staticlib (`libmoq_ffi.a` / `moq_ffi.lib`), and links the C++ runtime. +The archive holds the compiled C API (`libmoq.a` / `moq.lib`) and moq-ffi's staticlib, installed as `libmoq_c_ffi.a` / `moq_c_ffi.lib` so it never overwrites moq-cpp's, and links the C++ runtime. It embeds the same Rust runtime as the C++ package, `moq-cpp`, so a program links one of the two, never both. ## moq-c 0.7, the hand-written ABI diff --git a/c/moq/README.md b/c/moq/README.md index e5b17b0756c..a4c2f6c5296 100644 --- a/c/moq/README.md +++ b/c/moq/README.md @@ -18,6 +18,6 @@ The generator is the one the C++ package pins; see [cpp/moq](../../cpp/moq/READM ## Shape -The implementation is C++17 over the generated C++ bindings, compiled into `libmoq.a` (`moq.lib`) beside moq-ffi's `libmoq_ffi.a` (`moq_ffi.lib`), so a C program needs no C++ compiler, but its link adds the C++ runtime, which the CMake package and `moq-c.pc` name. Both libraries use the release CRT (`/MD`) on MSVC in every configuration. +The implementation is C++17 over the generated C++ bindings, compiled into `libmoq.a` (`moq.lib`) beside moq-ffi's staticlib, installed as `libmoq_c_ffi.a` (`moq_c_ffi.lib`) so moq-cpp's copy can share the prefix, so a C program needs no C++ compiler, but its link adds the C++ runtime, which the CMake package and `moq-c.pc` name. Both libraries use the release CRT (`/MD`) on MSVC in every configuration. This package and the C++ package, `moq-cpp`, each embed moq-ffi, so one program links one of them, never both, and they install to separate prefixes. diff --git a/c/moq/cmake/moq-c.pc.in b/c/moq/cmake/moq-c.pc.in index 8f8b2a0f5fb..aea94235375 100644 --- a/c/moq/cmake/moq-c.pc.in +++ b/c/moq/cmake/moq-c.pc.in @@ -6,4 +6,4 @@ Name: moq-c Description: Media over QUIC for C Version: @PROJECT_VERSION@ Cflags: -I${includedir} -Libs: -L${libdir} -lmoq -lmoq_ffi@MOQ_LIBS_PC@ +Libs: -L${libdir} -lmoq -lmoq_c_ffi@MOQ_LIBS_PC@ diff --git a/cpp/moq/CMakeLists.txt b/cpp/moq/CMakeLists.txt index 32b3f2da641..9ff6d77c54d 100644 --- a/cpp/moq/CMakeLists.txt +++ b/cpp/moq/CMakeLists.txt @@ -9,94 +9,7 @@ endif() project(moq-cpp VERSION ${_version} LANGUAGES CXX) set(_workspace "${CMAKE_CURRENT_SOURCE_DIR}/../..") -set(MOQ_FFI_DIR "" CACHE PATH "Extracted default-feature moq-ffi native package") -if(MOQ_FFI_DIR) - get_filename_component(MOQ_FFI_DIR "${MOQ_FFI_DIR}" ABSOLUTE) - file(READ "${MOQ_FFI_DIR}/manifest.json" _manifest) - foreach(_field version target features profile source) - string(JSON _input_${_field} GET "${_manifest}" ${_field}) - endforeach() - if(NOT _input_features STREQUAL "default" OR NOT _input_profile STREQUAL "release") - message(FATAL_ERROR "Prebuilt moq-ffi requires default features and release profile") - endif() - if(NOT _input_version MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+([-+][0-9A-Za-z.+-]+)?$") - message(FATAL_ERROR "Malformed prebuilt moq-ffi version: ${_input_version}") - endif() - if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") - set(_ffi_target aarch64-unknown-linux-gnu) - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") - set(_ffi_target x86_64-unknown-linux-gnu) - endif() - elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") - set(_ffi_target aarch64-apple-darwin) - elseif(MSVC AND CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|x86_64)$") - set(_ffi_target x86_64-pc-windows-msvc) - endif() - if(NOT DEFINED _ffi_target OR NOT _ffi_target STREQUAL _input_target) - message(FATAL_ERROR "Prebuilt moq-ffi target ${_input_target} does not match this host") - endif() -else() - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set(_profile debug) - set(_cargo_flags "") - else() - set(_profile release) - set(_cargo_flags --release) - endif() - # A build target, even the host's, moves the library under /, which cargo metadata - # does not report. A `[build] target` in a cargo config fails later, at the missing library. - if(DEFINED ENV{CARGO_BUILD_TARGET}) - message(FATAL_ERROR "Unsupported CARGO_BUILD_TARGET: moq-ffi builds for the host here; unset it") - endif() - # Ask cargo, so CARGO_TARGET_DIR (even relative) and a configured build.target-dir both land - # where the build below puts the library. - execute_process( - COMMAND cargo metadata --locked --no-deps --format-version 1 - WORKING_DIRECTORY "${_workspace}" - OUTPUT_VARIABLE _cargo_metadata - COMMAND_ERROR_IS_FATAL ANY - ) - string(JSON _cargo_target GET "${_cargo_metadata}" target_directory) -endif() - -if(MOQ_FFI_DIR) - set(_cargo_target "${MOQ_FFI_DIR}/lib") - set(_profile "") -endif() -if(WIN32) - set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/moq_ffi.lib") - set(_metadata "${_cargo_target}/${_profile}/moq_ffi.dll") -elseif(APPLE) - set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/libmoq_ffi.a") - set(_metadata "${_cargo_target}/${_profile}/libmoq_ffi.dylib") -else() - set(MOQ_FFI_LIB "${_cargo_target}/${_profile}/libmoq_ffi.a") - set(_metadata "${_cargo_target}/${_profile}/libmoq_ffi.so") -endif() -if(MOQ_FFI_DIR) - foreach(_input "${MOQ_FFI_LIB}" "${_metadata}" "${MOQ_FFI_DIR}/native-libs.txt") - if(NOT EXISTS "${_input}") - message(FATAL_ERROR "Missing prebuilt FFI input: ${_input}") - endif() - endforeach() - set(_cargo_ffi "") -else() - # The OpenH264 software fallback is opt-in in Rust; the C++ package ships it, like the - # prebuilt input does. - # One cargo target per build tree, so a project adding both c/moq and cpp/moq (which build - # the same moq-ffi) has one rule for its outputs, as Ninja requires. - if(NOT TARGET moq_ffi_cargo) - add_custom_target(moq_ffi_cargo - COMMAND cargo build --locked --package moq-ffi --features openh264 ${_cargo_flags} - WORKING_DIRECTORY "${_workspace}" - BYPRODUCTS "${MOQ_FFI_LIB}" "${_metadata}" - COMMENT "Building moq-ffi" - VERBATIM - ) - endif() - set(_cargo_ffi moq_ffi_cargo) -endif() +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/moq-ffi.cmake") find_program(UNIFFI_BINDGEN_CPP uniffi-bindgen-cpp REQUIRED) @@ -112,7 +25,7 @@ set(_render "${CMAKE_CURRENT_BINARY_DIR}/render") list(TRANSFORM _generated_files PREPEND "${_render}/" OUTPUT_VARIABLE _render_paths) add_custom_target(moq_ffi ALL COMMAND ${CMAKE_COMMAND} -E rm -rf "${_render}" - COMMAND "${UNIFFI_BINDGEN_CPP}" --library "${_metadata}" + COMMAND "${UNIFFI_BINDGEN_CPP}" --library "${MOQ_FFI_METADATA}" --config "${CMAKE_CURRENT_SOURCE_DIR}/uniffi.toml" --out-dir "${_render}" COMMAND ${CMAKE_COMMAND} -E make_directory "${_generated}" COMMAND ${CMAKE_COMMAND} -E copy_if_different ${_render_paths} "${_generated}" @@ -121,36 +34,9 @@ add_custom_target(moq_ffi ALL COMMENT "Rendering the moq-ffi C++ bindings" VERBATIM ) -if(_cargo_ffi) - add_dependencies(moq_ffi ${_cargo_ffi}) +if(MOQ_FFI_CARGO) + add_dependencies(moq_ffi ${MOQ_FFI_CARGO}) endif() -# System libraries the staticlib needs from an external linker. moq-ffi and moq-c share one -# dependency tree, so they share rs/moq-c/native-libs/, which says how to regenerate it. -if(APPLE) - set(_platform apple) -elseif(WIN32) - set(_platform windows) -else() - set(_platform linux) -endif() -if(MOQ_FFI_DIR) - set(_native_libs "${MOQ_FFI_DIR}/native-libs.txt") -else() - set(_native_libs "${_workspace}/rs/moq-c/native-libs/${_platform}.txt") -endif() -file(STRINGS "${_native_libs}" _lines) -set(MOQ_NATIVE_LIBS "") -foreach(_line IN LISTS _lines) - string(STRIP "${_line}" _line) - if(_line STREQUAL "" OR _line MATCHES "^#") - continue() - elseif(_line MATCHES "^framework:(.+)$") - list(APPEND MOQ_NATIVE_LIBS "-framework ${CMAKE_MATCH_1}") - else() - list(APPEND MOQ_NATIVE_LIBS "${_line}") - endif() -endforeach() - # The bindings compile here at the including project's CMAKE_CXX_STANDARD (else the compiler's # default, at least C++17) and with its flags. # Named moq-cpp so an in-tree build can sit beside moq-c's `moq` target. diff --git a/cpp/moq/cmake/moq-ffi.cmake b/cpp/moq/cmake/moq-ffi.cmake new file mode 100644 index 00000000000..fb9bf968436 --- /dev/null +++ b/cpp/moq/cmake/moq-ffi.cmake @@ -0,0 +1,150 @@ +# The moq-ffi native input shared by cpp/moq and c/moq: an extracted prebuilt package +# (MOQ_FFI_DIR), or a cargo build of this checkout. Sets MOQ_FFI_LIB (the staticlib to link), +# MOQ_FFI_METADATA (the cdylib uniffi-bindgen-cpp reads), MOQ_FFI_CARGO (the target that builds +# them, or empty for a prebuilt input), and MOQ_NATIVE_LIBS (what an external linker adds). +# +# Run with -P, it is that cargo build: it links the libraries cargo reports into OUT, so no +# cargo setting (target-dir, build.target) can move them from where the packages look. + +if(CMAKE_SCRIPT_MODE_FILE) + set(_args build --locked --package moq-ffi --features openh264 + --message-format=json-render-diagnostics) + if(RELEASE) + list(APPEND _args --release) + endif() + execute_process( + COMMAND cargo ${_args} + WORKING_DIRECTORY "${WORKSPACE}" + OUTPUT_VARIABLE _messages + COMMAND_ERROR_IS_FATAL ANY + ) + string(REGEX MATCH "{\"reason\":\"compiler-artifact\"[^\n]*\"name\":\"moq_ffi\",[^\n]*" + _artifact "${_messages}") + if(NOT _artifact) + message(FATAL_ERROR "cargo reported no moq_ffi library") + endif() + string(JSON _count LENGTH "${_artifact}" filenames) + foreach(_name IN ITEMS ${STATIC} ${DYNAMIC}) + set(_found "") + math(EXPR _last "${_count} - 1") + foreach(_i RANGE ${_last}) + string(JSON _file GET "${_artifact}" filenames ${_i}) + get_filename_component(_base "${_file}" NAME) + if(_base STREQUAL _name) + set(_found "${_file}") + endif() + endforeach() + if(NOT _found) + message(FATAL_ERROR "cargo built no ${_name} for moq_ffi") + endif() + # A hard link costs no space and keeps cargo's timestamp, so an unchanged library + # relinks nothing; a copy is the fallback across filesystems. Not a symlink, which + # install() would ship as a dangling link. + file(MAKE_DIRECTORY "${OUT}") + file(REMOVE "${OUT}/${_name}.tmp") + file(CREATE_LINK "${_found}" "${OUT}/${_name}.tmp" RESULT _linked) + if(_linked EQUAL 0) + file(RENAME "${OUT}/${_name}.tmp" "${OUT}/${_name}") + else() + file(COPY_FILE "${_found}" "${OUT}/${_name}" ONLY_IF_DIFFERENT) + endif() + endforeach() + return() +endif() + +if(WIN32) + set(_moq_ffi_static moq_ffi.lib) + set(_moq_ffi_dynamic moq_ffi.dll) +elseif(APPLE) + set(_moq_ffi_static libmoq_ffi.a) + set(_moq_ffi_dynamic libmoq_ffi.dylib) +else() + set(_moq_ffi_static libmoq_ffi.a) + set(_moq_ffi_dynamic libmoq_ffi.so) +endif() +get_filename_component(_moq_workspace "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) + +set(MOQ_FFI_DIR "" CACHE PATH "Extracted default-feature moq-ffi native package") +if(MOQ_FFI_DIR) + get_filename_component(MOQ_FFI_DIR "${MOQ_FFI_DIR}" ABSOLUTE) + file(READ "${MOQ_FFI_DIR}/manifest.json" _manifest) + foreach(_field version target features profile source) + string(JSON _input_${_field} GET "${_manifest}" ${_field}) + endforeach() + if(NOT _input_features STREQUAL "default" OR NOT _input_profile STREQUAL "release") + message(FATAL_ERROR "Prebuilt moq-ffi requires default features and release profile") + endif() + if(NOT _input_version MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+([-+][0-9A-Za-z.+-]+)?$") + message(FATAL_ERROR "Malformed prebuilt moq-ffi version: ${_input_version}") + endif() + unset(_ffi_target) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(_ffi_target aarch64-unknown-linux-gnu) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") + set(_ffi_target x86_64-unknown-linux-gnu) + endif() + elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(_ffi_target aarch64-apple-darwin) + elseif(MSVC AND CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|x86_64)$") + set(_ffi_target x86_64-pc-windows-msvc) + endif() + if(NOT DEFINED _ffi_target OR NOT _ffi_target STREQUAL _input_target) + message(FATAL_ERROR "Prebuilt moq-ffi target ${_input_target} does not match this host") + endif() + set(MOQ_FFI_LIB "${MOQ_FFI_DIR}/lib/${_moq_ffi_static}") + set(MOQ_FFI_METADATA "${MOQ_FFI_DIR}/lib/${_moq_ffi_dynamic}") + set(_native_libs "${MOQ_FFI_DIR}/native-libs.txt") + foreach(_input "${MOQ_FFI_LIB}" "${MOQ_FFI_METADATA}" "${_native_libs}") + if(NOT EXISTS "${_input}") + message(FATAL_ERROR "Missing prebuilt FFI input: ${_input}") + endif() + endforeach() + set(MOQ_FFI_CARGO "") +else() + # The top-level build tree, so a project adding both packages shares one cargo build and + # one rule for its outputs, as Ninja requires. + set(_out "${CMAKE_BINARY_DIR}/moq-ffi") + set(MOQ_FFI_LIB "${_out}/${_moq_ffi_static}") + set(MOQ_FFI_METADATA "${_out}/${_moq_ffi_dynamic}") + set(MOQ_FFI_CARGO moq_ffi_cargo) + if(NOT TARGET moq_ffi_cargo) + # Multi-config generators leave CMAKE_BUILD_TYPE empty and get a release build. + set(_release ON) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(_release OFF) + endif() + # The packages ship the OpenH264 software fallback, like the prebuilt input does. + add_custom_target(moq_ffi_cargo + COMMAND "${CMAKE_COMMAND}" -DWORKSPACE=${_moq_workspace} -DOUT=${_out} + -DSTATIC=${_moq_ffi_static} -DDYNAMIC=${_moq_ffi_dynamic} -DRELEASE=${_release} + -P "${CMAKE_CURRENT_LIST_FILE}" + BYPRODUCTS "${MOQ_FFI_LIB}" "${MOQ_FFI_METADATA}" + COMMENT "Building moq-ffi" + VERBATIM + ) + endif() + # System libraries the staticlib needs from an external linker. moq-ffi and moq-c share one + # dependency tree, so they share rs/moq-c/native-libs/, which says how to regenerate it. + if(APPLE) + set(_platform apple) + elseif(WIN32) + set(_platform windows) + else() + set(_platform linux) + endif() + set(_native_libs "${_moq_workspace}/rs/moq-c/native-libs/${_platform}.txt") +endif() + +file(STRINGS "${_native_libs}" _lines) +set(MOQ_NATIVE_LIBS "") +foreach(_line IN LISTS _lines) + string(STRIP "${_line}" _line) + if(_line STREQUAL "" OR _line MATCHES "^#") + continue() + elseif(_line MATCHES "^framework:(.+)$") + list(APPEND MOQ_NATIVE_LIBS "-framework ${CMAKE_MATCH_1}") + else() + list(APPEND MOQ_NATIVE_LIBS "${_line}") + endif() +endforeach() From 3bc9327ea8101aedd999b2cf0c14a8aafee6028d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 02:27:46 -0700 Subject: [PATCH 14/25] fix(c): document /MD for MSVC consumers and probe a Debug build The package's C++ and Rust objects use the release CRT in every configuration, so a default Debug MSVC consumer (/MDd) fails with LNK2038. Say so, as the C++ package's docs do, and have the Windows probe build Debug as well as Release with the documented runtime. Co-Authored-By: Claude Opus 5.5 --- c/moq/README.md | 2 +- doc/lib/c/index.md | 5 ++++- sh/c/check.sh | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/c/moq/README.md b/c/moq/README.md index a4c2f6c5296..f7c290a3f8a 100644 --- a/c/moq/README.md +++ b/c/moq/README.md @@ -18,6 +18,6 @@ The generator is the one the C++ package pins; see [cpp/moq](../../cpp/moq/READM ## Shape -The implementation is C++17 over the generated C++ bindings, compiled into `libmoq.a` (`moq.lib`) beside moq-ffi's staticlib, installed as `libmoq_c_ffi.a` (`moq_c_ffi.lib`) so moq-cpp's copy can share the prefix, so a C program needs no C++ compiler, but its link adds the C++ runtime, which the CMake package and `moq-c.pc` name. Both libraries use the release CRT (`/MD`) on MSVC in every configuration. +The implementation is C++17 over the generated C++ bindings, compiled into `libmoq.a` (`moq.lib`) beside moq-ffi's staticlib, installed as `libmoq_c_ffi.a` (`moq_c_ffi.lib`) so moq-cpp's copy can share the prefix, so a C program needs no C++ compiler, but its link adds the C++ runtime, which the CMake package and `moq-c.pc` name. Both libraries use the release CRT (`/MD`) on MSVC in every configuration, so an MSVC consumer must too (`CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL`); its default Debug runtime (`/MDd`) fails to link with LNK2038. This package and the C++ package, `moq-cpp`, each embed moq-ffi, so one program links one of them, never both, and they install to separate prefixes. diff --git a/doc/lib/c/index.md b/doc/lib/c/index.md index ed2c9cbb38d..ddef6c7e51b 100644 --- a/doc/lib/c/index.md +++ b/doc/lib/c/index.md @@ -44,7 +44,10 @@ The next release, 0.8, replaces this ABI with a C API generated from with the same `moq::c` target and `moq-c.pc`, and its [MIGRATE.md](https://github.com/moq-dev/moq/blob/main/c/moq/MIGRATE.md) maps each call. To try it from a checkout, `just c package` builds the archive from -[`c/moq`](https://github.com/moq-dev/moq/tree/main/c/moq). +[`c/moq`](https://github.com/moq-dev/moq/tree/main/c/moq). On MSVC, build your +program with the release runtime (`/MD`, or +`CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL`) in every configuration, since +the package's C++ and Rust objects use it. ## Connect diff --git a/sh/c/check.sh b/sh/c/check.sh index 10a5d8d756f..309897bdd35 100755 --- a/sh/c/check.sh +++ b/sh/c/check.sh @@ -25,8 +25,8 @@ fi cd "$(dirname "${BASH_SOURCE[0]}")/../../c" -# Single-config generators build Debug. Visual Studio ignores CMAKE_BUILD_TYPE and gets -# --config Release, since the Rust staticlib links the release CRT (/MD) either way. +# Single-config generators build Debug. Visual Studio ignores CMAKE_BUILD_TYPE and builds the +# package with --config Release, since the Rust staticlib links the release CRT (/MD) either way. build=moq/build libdir=lib64 if [[ -n "$ffi_dir" ]]; then build=moq/build/prebuilt; fi @@ -65,15 +65,23 @@ if [[ -z "$archive" && -z "$ffi_dir" ]] && command -v ninja >/dev/null 2>&1; the fi run() { - if [[ -x "$1/probe" ]]; then "$1/probe"; else "$1/Release/probe.exe"; fi + if [[ -x "$1/probe" ]]; then "$1/probe"; else "$1/${2:-Release}/probe.exe"; fi } # Debian-family CMake never searches lib64/ under a prefix, so a lib64 install is found by its # config directory, as a Debian user of one would have to. find=(-DCMAKE_PREFIX_PATH="$prefix") [[ "$libdir" == lib ]] || find=(-Dmoq-c_DIR="$prefix/$libdir/cmake/moq-c") -cmake -S moq/test -B "$build/test" -DCMAKE_BUILD_TYPE=Debug "${find[@]}" +# An MSVC consumer links the release runtime (/MD) in every configuration, as the docs say, +# since the package's C++ and Rust objects do. +cmake -S moq/test -B "$build/test" -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL "${find[@]}" cmake --build "$build/test" --config Release run "$build/test" +# Visual Studio also builds Debug, the configuration whose default runtime (/MDd) mismatches. +if [[ ! -x "$build/test/probe" ]]; then + cmake --build "$build/test" --config Debug + run "$build/test" Debug +fi # The rest builds with `cc`, which on Windows is MinGW GCC and can't link the MSVC-built # package. MSVC consumers use the CMake package. From 734e3e8e32038a61d86537012deeaefe74ce4ab9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 11:17:46 -0700 Subject: [PATCH 15/25] fix(c,cpp): refuse moq-ffi source builds for a non-host target The wrappers compile for CMake's system while cargo builds for its own target, so a mismatch failed late at the link. A cross-compiling project now fails at configure, pointing at a prebuilt MOQ_FFI_DIR, and the cargo step fails when cargo's artifact lands under another triple than the host's. `just c check` configures a cross build and expects the refusal. Co-Authored-By: Claude Opus 5.5 --- cpp/moq/cmake/moq-ffi.cmake | 22 ++++++++++++++++++++++ sh/c/check.sh | 13 +++++++++++++ 2 files changed, 35 insertions(+) diff --git a/cpp/moq/cmake/moq-ffi.cmake b/cpp/moq/cmake/moq-ffi.cmake index fb9bf968436..80c6b913baa 100644 --- a/cpp/moq/cmake/moq-ffi.cmake +++ b/cpp/moq/cmake/moq-ffi.cmake @@ -24,6 +24,23 @@ if(CMAKE_SCRIPT_MODE_FILE) message(FATAL_ERROR "cargo reported no moq_ffi library") endif() string(JSON _count LENGTH "${_artifact}" filenames) + # The wrappers compile for the host, so moq-ffi must too. A build target (CARGO_BUILD_TARGET + # or a cargo config's build.target) puts the library under ///. + string(JSON _file GET "${_artifact}" filenames 0) + get_filename_component(_dir "${_file}" DIRECTORY) + get_filename_component(_dir "${_dir}" DIRECTORY) + get_filename_component(_triple "${_dir}" NAME) + execute_process(COMMAND rustc -vV OUTPUT_VARIABLE _rustc COMMAND_ERROR_IS_FATAL ANY) + string(REGEX MATCH "host: ([^\r\n]+)" _ "${_rustc}") + set(_host "${CMAKE_MATCH_1}") + execute_process(COMMAND rustc --print target-list OUTPUT_VARIABLE _targets + COMMAND_ERROR_IS_FATAL ANY) + string(REPLACE "\r" "" _targets "${_targets}") + string(REPLACE "\n" ";" _targets "${_targets}") + if(NOT _triple STREQUAL _host AND _triple IN_LIST _targets) + message(FATAL_ERROR "cargo built moq-ffi for ${_triple}, not the host (${_host}); a " + "source build targets the host, so unset the cargo build target or use MOQ_FFI_DIR") + endif() foreach(_name IN ITEMS ${STATIC} ${DYNAMIC}) set(_found "") math(EXPR _last "${_count} - 1") @@ -102,6 +119,11 @@ if(MOQ_FFI_DIR) endforeach() set(MOQ_FFI_CARGO "") else() + # Cargo builds for the host, so a cross-compiling project stages a prebuilt input instead. + if(CMAKE_CROSSCOMPILING) + message(FATAL_ERROR "A source build of moq-ffi targets the host; when cross-compiling, " + "set MOQ_FFI_DIR to a native package for the target") + endif() # The top-level build tree, so a project adding both packages shares one cargo build and # one rule for its outputs, as Ninja requires. set(_out "${CMAKE_BINARY_DIR}/moq-ffi") diff --git a/sh/c/check.sh b/sh/c/check.sh index 309897bdd35..c50b387ec42 100755 --- a/sh/c/check.sh +++ b/sh/c/check.sh @@ -64,6 +64,19 @@ if [[ -z "$archive" && -z "$ffi_dir" ]] && command -v ninja >/dev/null 2>&1; the ninja -C "$build/superbuild" -n >/dev/null fi +# Naming the system makes CMake cross-compile, which a source build of moq-ffi refuses at +# configure, pointing at a prebuilt input. Windows' generator is left alone. +if [[ -z "$archive" && -z "$ffi_dir" && "$(uname -s)" != MINGW* && "$(uname -s)" != MSYS* ]]; then + if cmake -S moq -B "$build/cross" -DCMAKE_SYSTEM_NAME="$(uname -s)" >"$build/cross.log" 2>&1; then + echo "A cross-compiling source build configured instead of refusing" >&2 + exit 1 + fi + grep -q MOQ_FFI_DIR "$build/cross.log" || { + cat "$build/cross.log" >&2 + exit 1 + } +fi + run() { if [[ -x "$1/probe" ]]; then "$1/probe"; else "$1/${2:-Release}/probe.exe"; fi } From a32e9ff01b865792abb077e9c14ebb01d9de4702 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 11:25:57 -0700 Subject: [PATCH 16/25] fix(c,cpp): find the build target relative to cargo's target directory A target directory named like a triple, such as CARGO_TARGET_DIR=/tmp/aarch64-unknown-linux-gnu, aborted a host build. The triple is now the component between cargo metadata's target_directory and the profile, compared with rustc's host. `just c check` builds through such a directory. Co-Authored-By: Claude Opus 5.5 --- cpp/moq/cmake/moq-ffi.cmake | 34 +++++++++++++++++++++------------- sh/c/check.sh | 5 +++++ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/cpp/moq/cmake/moq-ffi.cmake b/cpp/moq/cmake/moq-ffi.cmake index 80c6b913baa..613955c8870 100644 --- a/cpp/moq/cmake/moq-ffi.cmake +++ b/cpp/moq/cmake/moq-ffi.cmake @@ -25,21 +25,29 @@ if(CMAKE_SCRIPT_MODE_FILE) endif() string(JSON _count LENGTH "${_artifact}" filenames) # The wrappers compile for the host, so moq-ffi must too. A build target (CARGO_BUILD_TARGET - # or a cargo config's build.target) puts the library under ///. + # or a cargo config's build.target) puts the library under /// + # rather than //. + execute_process( + COMMAND cargo metadata --locked --no-deps --format-version 1 + WORKING_DIRECTORY "${WORKSPACE}" + OUTPUT_VARIABLE _metadata + COMMAND_ERROR_IS_FATAL ANY + ) + string(JSON _target_dir GET "${_metadata}" target_directory) string(JSON _file GET "${_artifact}" filenames 0) + file(TO_CMAKE_PATH "${_target_dir}" _target_dir) + file(TO_CMAKE_PATH "${_file}" _file) get_filename_component(_dir "${_file}" DIRECTORY) - get_filename_component(_dir "${_dir}" DIRECTORY) - get_filename_component(_triple "${_dir}" NAME) - execute_process(COMMAND rustc -vV OUTPUT_VARIABLE _rustc COMMAND_ERROR_IS_FATAL ANY) - string(REGEX MATCH "host: ([^\r\n]+)" _ "${_rustc}") - set(_host "${CMAKE_MATCH_1}") - execute_process(COMMAND rustc --print target-list OUTPUT_VARIABLE _targets - COMMAND_ERROR_IS_FATAL ANY) - string(REPLACE "\r" "" _targets "${_targets}") - string(REPLACE "\n" ";" _targets "${_targets}") - if(NOT _triple STREQUAL _host AND _triple IN_LIST _targets) - message(FATAL_ERROR "cargo built moq-ffi for ${_triple}, not the host (${_host}); a " - "source build targets the host, so unset the cargo build target or use MOQ_FFI_DIR") + file(RELATIVE_PATH _within "${_target_dir}" "${_dir}") + if(_within MATCHES "^([^/]+)/[^/]+$") + set(_triple "${CMAKE_MATCH_1}") + execute_process(COMMAND rustc -vV OUTPUT_VARIABLE _rustc COMMAND_ERROR_IS_FATAL ANY) + string(REGEX MATCH "host: ([^\r\n]+)" _ "${_rustc}") + if(NOT _triple STREQUAL CMAKE_MATCH_1) + message(FATAL_ERROR "cargo built moq-ffi for ${_triple}, not the host " + "(${CMAKE_MATCH_1}); a source build targets the host, so unset the cargo build " + "target or use MOQ_FFI_DIR") + endif() endif() foreach(_name IN ITEMS ${STATIC} ${DYNAMIC}) set(_found "") diff --git a/sh/c/check.sh b/sh/c/check.sh index c50b387ec42..e6b92f86a2f 100755 --- a/sh/c/check.sh +++ b/sh/c/check.sh @@ -75,6 +75,11 @@ if [[ -z "$archive" && -z "$ffi_dir" && "$(uname -s)" != MINGW* && "$(uname -s)" cat "$build/cross.log" >&2 exit 1 } + # A target directory named like a triple is still a host build, and reuses cargo's output. + target_dir=$(cargo metadata --locked --no-deps --format-version 1 | sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p') + mkdir -p "$build/odd" + ln -sfn "$target_dir" "$build/odd/aarch64-unknown-linux-gnu" + CARGO_TARGET_DIR="$PWD/$build/odd/aarch64-unknown-linux-gnu" cmake --build "$build/package" --target moq_ffi_cargo fi run() { From 48a5dc14abea2bec47a353077e8bc8bc4ba7df37 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 11:32:09 -0700 Subject: [PATCH 17/25] doc(c): keep the unreleased 0.8 package out of the user docs The published moq-c is still 0.7; c/moq ships at a later release cut, when doc/lib/c is rewritten for it. Until then its README and MIGRATE.md carry the plan. Co-Authored-By: Claude Opus 5.5 --- doc/lib/c/index.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/doc/lib/c/index.md b/doc/lib/c/index.md index 73b6dd3457c..e094710f323 100644 --- a/doc/lib/c/index.md +++ b/doc/lib/c/index.md @@ -39,16 +39,6 @@ pkg-config file, and omits the OpenH264 software fallback unless given `moq-c` and the C++ package each link in their own copy of the Rust core, so a program links one of the two, never both. -The next release, 0.8, replaces this ABI with a C API generated from -`moq-ffi`, the crate every other binding wraps. It installs as `` -with the same `moq::c` target and `moq-c.pc`, and its -[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/c/moq/MIGRATE.md) maps -each call. To try it from a checkout, `just c package` builds the archive from -[`c/moq`](https://github.com/moq-dev/moq/tree/main/c/moq). On MSVC, build your -program with the release runtime (`/MD`, or -`CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL`) in every configuration, since -the package's C++ and Rust objects use it. - ## Connect ```c From 72283ad246c1ad0836aef997187bd4573bbdb736 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 22 Oct 2026 13:42:49 -0700 Subject: [PATCH 18/25] feat(c): keep a retrying first dial's failure in the generated package The probe polls moq_client_failure while a dial to a closed port retries, and MIGRATE.md maps the hand-written moq_session_failure to it. The probe also expects an untimed frame, since #6150 drops received moq-lite-06 timestamps. Co-Authored-By: Claude Opus 5.5 --- c/moq/MIGRATE.md | 1 + c/moq/test/probe.c | 34 ++++++++++++++++++++++++++--- quest/m1/api-audit/binding-state.md | 3 +++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/c/moq/MIGRATE.md b/c/moq/MIGRATE.md index aaeaff8831c..8355e8f18ce 100644 --- a/c/moq/MIGRATE.md +++ b/c/moq/MIGRATE.md @@ -31,6 +31,7 @@ A few common calls: | `moq_origin_create_broadcast` | `moq_origin_producer_create_broadcast` | | `moq_publish_track` | `moq_broadcast_producer_publish_track` | | `moq_session_connect` | `moq_client_new`, then `moq_client_connect` | +| `moq_session_failure` | `moq_client_failure` while the first dial retries, then `moq_session_failure`; each returns the last failure as a `moq_error *`, or NULL | | `moq_server_listen` | `moq_server_new`, `moq_server_listen`, then `moq_server_accept` per session | | `moq_consume_track` | `moq_broadcast_consumer_subscribe_track`, then `moq_track_consumer_read_frame` per frame | diff --git a/c/moq/test/probe.c b/c/moq/test/probe.c index 6f073eec3c9..775b792dabf 100644 --- a/c/moq/test/probe.c +++ b/c/moq/test/probe.c @@ -1,6 +1,7 @@ /* Exercises the installed package end to end over a real QUIC session from plain C: a returned - * error, a loopback connect, a frame read through a callback, a cancelled read, a callback - * in flight at shutdown, and every callback run on this thread through a custom dispatcher. */ + * error, a loopback connect, a frame read through a callback, a cancelled read, a retrying + * first dial's failure, a callback in flight at shutdown, and every callback run on this + * thread through a custom dispatcher. */ /* clock_gettime and the pthread timed wait under strict C11. */ #define _POSIX_C_SOURCE 200809L @@ -324,7 +325,8 @@ int main(void) { CHECK(received); CHECK(received->payload.len == sizeof(hello)); CHECK(memcmp(received->payload.data, hello, sizeof(hello)) == 0); - CHECK(received->timestamp_us.has_value && received->timestamp_us.value == 1000); + /* The default moq-lite-06 session drops received timestamps, so the track arrives untimed. */ + CHECK(!received->timestamp_us.has_value); moq_frame_free(received); CHECK(!cancelled.done); @@ -365,6 +367,32 @@ int main(void) { moq_broadcast_producer_free(broadcast); moq_origin_producer_free(origin); + /* A first dial that keeps failing never resolves connect, so moq_client_failure reports + * its last failure; the server is gone, so every dial fails. */ + moq_client_config retry_config = moq_client_config_default(); + retry_config.timeout_us.has_value = true; + retry_config.timeout_us.value = 100000; + retry_config.websocket.enabled.has_value = true; + retry_config.websocket.enabled.value = false; + moq_client *retrying = NULL; + ok(moq_client_new(&retry_config, &retrying), "client new"); + CHECK(moq_client_failure(retrying) == NULL); + result dialing = {0}; + task = moq_client_connect(retrying, url, on_session, &dialing); + moq_error *failure = NULL; + for (int waited = 0; !failure && waited < 5000; waited += 10) { + CHECK(!pump(&dialing.done, 10)); + failure = moq_client_failure(retrying); + } + CHECK(failure && failure->tag == MOQ_ERROR_CONNECT); + moq_error_free(failure); + moq_client_cancel(retrying); + CHECK(pump(&dialing.done, 5000)); + CHECK(dialing.error && dialing.error->tag == MOQ_ERROR_CANCELLED); + moq_error_free(dialing.error); + moq_task_free(task); + moq_client_free(retrying); + /* A read whose frame arrives just before shutdown has its callback in flight, so it may run * while moq_shutdown drains the dispatcher; none may run once it returns, even as the * handles are freed afterward. */ diff --git a/quest/m1/api-audit/binding-state.md b/quest/m1/api-audit/binding-state.md index 7c5221d14b5..47bfb2202c8 100644 --- a/quest/m1/api-audit/binding-state.md +++ b/quest/m1/api-audit/binding-state.md @@ -22,6 +22,9 @@ and Go hide the `Client`, so the first-dial failure is unreachable there. `MoqClient::failure` goes away. - Name fields after moq-tokio's `Monitor` (`presence`, not `status`). - Update every wrapper, OBS, and `doc/lib/*`. +- The generated C package (`c/moq`) probes `moq_client_failure` and maps the + hand-written `moq_session_failure` to it in `c/moq/MIGRATE.md`; point both + at the state snapshot's last failure. Public API: moq-ffi, moq-c, every wrapper, OBS breaking. Wire: none. From fb5c92b94fae1abd43e8c47ac124498e8f8d07ea Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 22 Oct 2026 13:44:09 -0700 Subject: [PATCH 19/25] fix(c,cpp): refuse a source build for a 32-bit or other-architecture compiler Visual Studio's -A Win32 or gcc -m32 on a 64-bit host leaves CMAKE_CROSSCOMPILING false, so cargo would build a host-width moq-ffi that fails only at link time. Co-Authored-By: Claude Opus 5.5 --- cpp/moq/cmake/moq-ffi.cmake | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/cpp/moq/cmake/moq-ffi.cmake b/cpp/moq/cmake/moq-ffi.cmake index 613955c8870..b31e4dfceea 100644 --- a/cpp/moq/cmake/moq-ffi.cmake +++ b/cpp/moq/cmake/moq-ffi.cmake @@ -128,9 +128,19 @@ if(MOQ_FFI_DIR) set(MOQ_FFI_CARGO "") else() # Cargo builds for the host, so a cross-compiling project stages a prebuilt input instead. - if(CMAKE_CROSSCOMPILING) - message(FATAL_ERROR "A source build of moq-ffi targets the host; when cross-compiling, " - "set MOQ_FFI_DIR to a native package for the target") + # A 32-bit or other-architecture compiler on the same OS (`-m32`, Visual Studio's `-A Win32`) + # leaves CMAKE_CROSSCOMPILING false, so compare the compiler with a 64-bit host too. + unset(_host_arch) + if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") + set(_host_arch x64) + elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") + set(_host_arch ARM64) + endif() + if(CMAKE_CROSSCOMPILING + OR (DEFINED _host_arch AND NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + OR (DEFINED _host_arch AND MSVC AND NOT CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL _host_arch)) + message(FATAL_ERROR "A source build of moq-ffi targets the host; for another target " + "or architecture, set MOQ_FFI_DIR to a native package for it") endif() # The top-level build tree, so a project adding both packages shares one cargo build and # one rule for its outputs, as Ninja requires. From db4039d1687aee965a740f4c33fa5519027b8645 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 22 Oct 2026 13:52:57 -0700 Subject: [PATCH 20/25] quest(c): unblock the async failure contract once the package lands Co-Authored-By: Claude Opus 5.5 --- quest/m1/c/async-failure-contract.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/quest/m1/c/async-failure-contract.md b/quest/m1/c/async-failure-contract.md index 94f3c00848c..ee2bbfb4373 100644 --- a/quest/m1/c/async-failure-contract.md +++ b/quest/m1/c/async-failure-contract.md @@ -20,7 +20,3 @@ From the 2026-10-22 security audit: Tests: a call refused before it starts fires no callback; a call whose work panics still delivers one terminal callback; freeing a pending task, including from inside another callback, delivers one cancelled callback. Public API: header documentation and a guaranteed terminal callback. Wire: none. - -## Required - -- [moq-c package](/quest/m1/c/package.md) - the generated C API this contract applies to From 0dcd71352a32d9ab44a679406900978b75bbfc8c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 16:53:59 -0700 Subject: [PATCH 21/25] docs(c): carry the hand-written ABI's newer MIGRATE entries The hand-written crate never published as moq-c, so a libmoq 0.6 user needs every entry since, not only those up to #5677. Co-Authored-By: Claude Opus 5.5 --- c/moq/MIGRATE.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/c/moq/MIGRATE.md b/c/moq/MIGRATE.md index 8355e8f18ce..1074f1f3fc4 100644 --- a/c/moq/MIGRATE.md +++ b/c/moq/MIGRATE.md @@ -55,6 +55,68 @@ It embeds the same Rust runtime as the C++ package, `moq-cpp`, so a program link ## moq-c 0.7, the hand-written ABI +The hand-written crate never published as moq-c: a libmoq 0.6 user applies these entries, then the table above. + +### Video rendition preference + +`moq_video_config` gains `int32_t preference`. Initialize it to `0` to keep the default ranking; higher values are preferred and compatibility transcodes use `-1`. Rebuild C consumers against the matching header and library because the struct layout changes. + +### A broadcast reference waits for its sibling's announcement (#6254) + +`moq_consume_video` and `moq_consume_audio` on a rendition whose catalog `broadcast` field names a sibling now wait for a sibling nothing announces yet instead of failing as unroutable, so a late sibling plays once it is announced. +Where you skipped such a rendition on that error, give up on it yourself with `moq_consume_video_cancel` or `moq_consume_audio_cancel`. + +### JSON snapshot `delta_ratio` is `deltas` + +`moq_json_snapshot_config.delta_ratio` is replaced by `bool deltas`. +Set `deltas = false` where you set `delta_ratio = 0`, and `deltas = true` for any positive ratio. +With deltas on, the track carries a refresh ratio of 8 and the producer restates the value in a new group when a joiner asks. + +### Priorities are signed (#6142) + +`moq_track_info.priority` and `moq_subscription.priority` are `int8_t`, and `moq_group_request_priority` writes an `int8_t`, which changes the ABI. +Higher is first and 0 is the unset midpoint, so a zeroed `moq_track_info` now uses the default priority rather than the least urgent one. +To keep a value's moq-lite byte, subtract 128 from it: 255 becomes 127, 128 becomes 0, and 0 becomes -128. + +### A client asks only about what it watches (#6165) + +On moq-lite, a client session asks its relay for announcements only under paths something watches, through `moq_origin_announced` or `moq_origin_announced_broadcast`, instead of under its whole scope at connect. +`moq_origin_request` watches nothing, so on a client it fails with a negative code for a path nothing watches, even when the relay announces it. +Resolve the path with `moq_origin_announced_broadcast`, or hold `moq_origin_announced` over its prefix while requesting. +moq-transport and moq-lite-03/04 sessions are unchanged. + +### `MOQ_CONTAINER_FORMAT_FMP4` unwraps to Legacy (#6250) + +A container import with `MOQ_CONTAINER_FORMAT_FMP4` now publishes one Legacy frame per sample instead of one CMAF frame per fragment. +Pass the new `MOQ_CONTAINER_FORMAT_CMAF` to keep publishing CMAF. + +### Timestamps stop at 2^61 - 1 microseconds (#6168) + +Writing a frame, or any other call that takes a timestamp, fails with `MOQ_ERROR_TIMESTAMP_OVERFLOW` past `2^61 - 1` microseconds instead of `2^62 - 1`, the most moq-lite-05/06 can carry. +A received timestamp past it fails the group as a malformed track. +Clamp timestamps you build from unbounded input to `2^61 - 1`. + +### Finished tracks stay until the broadcast ends (#6157) + +`moq_publish_track_finish`, `moq_publish_media_finish`, `moq_publish_container_finish`, and the encoder finishes now release the handle while the broadcast keeps the finished track listed, with its catalog rendition and demand, until `moq_publish_close` or the new `moq_publish_finish_close`. +Publish a replacement under a new name, and expect a finished track's demand terminal callback when the broadcast closes rather than at the finish. + +### A claim answers by publishing (#5678) + +`moq_origin_dynamic` takes no route, and `moq_origin_dynamic_update` is gone. +`moq_broadcast_request_accept` is replaced by `moq_broadcast_request_prepare`, which returns a prepared answer under the epoch you pass: fill its broadcast, then `moq_broadcast_prepared_publish` announces it at the exact path under that epoch. +`moq_broadcast_prepared_free` refuses an answer you will not publish. + +### `moq_track_info` gains `compress` (#5928) + +The new trailing field changes its ABI. A zero-initialized struct leaves it false, as before. + +### Anchors can name a time (#6074) + +`moq_anchor` gains `timestamp_us`, read with the new `MOQ_ANCHOR_TIME` kind, which changes its ABI. +`moq_consume_track_update` fails with a negative code when the track refuses an update, such as a time anchor on an untimed track, leaving the subscription as it was. +An update made before the subscription is established applies once it is, so a refusal then ends the consumer through its callback. + ### JSON values carry their timestamp (#5677) `moq_json_value` gains `timestamp_us` and `timestamp_present`, which changes its ABI. From daae0a9bfab99a291f9f44e6d6709162812b79aa Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 16:53:59 -0700 Subject: [PATCH 22/25] fix(c,cpp): compare the compiler's architecture with both native inputs CMAKE_OSX_ARCHITECTURES, like -m32 or Visual Studio's -A, changes the compiled architecture without setting CMAKE_CROSSCOMPILING. Normalize the compiler's architecture once and check it against the host for a source build and against the manifest target for a prebuilt input, which matched CMAKE_SYSTEM_PROCESSOR and so accepted a mismatched package too. Co-Authored-By: Claude Opus 5.5 --- cpp/moq/cmake/moq-ffi.cmake | 48 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/cpp/moq/cmake/moq-ffi.cmake b/cpp/moq/cmake/moq-ffi.cmake index b31e4dfceea..3395e806ad1 100644 --- a/cpp/moq/cmake/moq-ffi.cmake +++ b/cpp/moq/cmake/moq-ffi.cmake @@ -89,6 +89,28 @@ else() endif() get_filename_component(_moq_workspace "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) +# The architecture the compiler targets, normalized to x86_64 or aarch64. A same-OS override +# (`-m32`, Visual Studio's `-A`, CMAKE_OSX_ARCHITECTURES) changes it without changing +# CMAKE_SYSTEM_PROCESSOR or setting CMAKE_CROSSCOMPILING, so both inputs compare against it. +set(_moq_arch "${CMAKE_SYSTEM_PROCESSOR}") +if(CMAKE_CXX_COMPILER_ARCHITECTURE_ID) + set(_moq_arch "${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}") +endif() +if(APPLE AND CMAKE_OSX_ARCHITECTURES) + set(_moq_arch "${CMAKE_OSX_ARCHITECTURES}") +endif() +set(_moq_host_arch "${CMAKE_HOST_SYSTEM_PROCESSOR}") +foreach(_var _moq_arch _moq_host_arch) + if(${_var} MATCHES "^(x86_64|AMD64|amd64|x64)$") + set(${_var} x86_64) + elseif(${_var} MATCHES "^(aarch64|arm64|ARM64)$") + set(${_var} aarch64) + endif() +endforeach() +if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_moq_arch "${CMAKE_SIZEOF_VOID_P}-byte pointers") +endif() + set(MOQ_FFI_DIR "" CACHE PATH "Extracted default-feature moq-ffi native package") if(MOQ_FFI_DIR) get_filename_component(MOQ_FFI_DIR "${MOQ_FFI_DIR}" ABSOLUTE) @@ -103,15 +125,11 @@ if(MOQ_FFI_DIR) message(FATAL_ERROR "Malformed prebuilt moq-ffi version: ${_input_version}") endif() unset(_ffi_target) - if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") - set(_ffi_target aarch64-unknown-linux-gnu) - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") - set(_ffi_target x86_64-unknown-linux-gnu) - endif() - elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND _moq_arch MATCHES "^(x86_64|aarch64)$") + set(_ffi_target ${_moq_arch}-unknown-linux-gnu) + elseif(APPLE AND _moq_arch STREQUAL "aarch64") set(_ffi_target aarch64-apple-darwin) - elseif(MSVC AND CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|x86_64)$") + elseif(MSVC AND _moq_arch STREQUAL "x86_64") set(_ffi_target x86_64-pc-windows-msvc) endif() if(NOT DEFINED _ffi_target OR NOT _ffi_target STREQUAL _input_target) @@ -127,18 +145,8 @@ if(MOQ_FFI_DIR) endforeach() set(MOQ_FFI_CARGO "") else() - # Cargo builds for the host, so a cross-compiling project stages a prebuilt input instead. - # A 32-bit or other-architecture compiler on the same OS (`-m32`, Visual Studio's `-A Win32`) - # leaves CMAKE_CROSSCOMPILING false, so compare the compiler with a 64-bit host too. - unset(_host_arch) - if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") - set(_host_arch x64) - elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") - set(_host_arch ARM64) - endif() - if(CMAKE_CROSSCOMPILING - OR (DEFINED _host_arch AND NOT CMAKE_SIZEOF_VOID_P EQUAL 8) - OR (DEFINED _host_arch AND MSVC AND NOT CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL _host_arch)) + # Cargo builds for the host, so a project targeting anything else stages a prebuilt input. + if(CMAKE_CROSSCOMPILING OR NOT _moq_arch STREQUAL _moq_host_arch) message(FATAL_ERROR "A source build of moq-ffi targets the host; for another target " "or architecture, set MOQ_FFI_DIR to a native package for it") endif() From 39c392a1d6a5e50617f7adb1f81abaf2965d8f9d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 17:24:17 -0700 Subject: [PATCH 23/25] test(gh): list the c workflow among the release-gate calls Co-Authored-By: Claude Opus 5.5 --- sh/gh/queue.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/sh/gh/queue.test.ts b/sh/gh/queue.test.ts index 1dea03014dd..2c9cf42282a 100644 --- a/sh/gh/queue.test.ts +++ b/sh/gh/queue.test.ts @@ -70,6 +70,7 @@ for (const [id, job] of calls) { assert.deepEqual(calls.map(([id]) => id).sort(), [ "android", "audio-quality", + "c", "cpp", "features", "interop", From e8b7faf07580daf4c657381fa21c40e72fd3c322 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 15:35:28 -0700 Subject: [PATCH 24/25] fix(c): adopt main's connection state, SETUP naming, and cut-time MIGRATE The probe observes a retrying first dial through moq_session_state and moq_session_state_changed, ending closed, now that moq_client_failure is gone. The package names itself through MOQ_IMPLEMENTATION, like moq::IMPLEMENTATION. MIGRATE.md entries move to the PR's Impact section, written at the release cut. Co-Authored-By: Claude Opus 5.5 --- c/moq/CMakeLists.txt | 4 +++ c/moq/MIGRATE.md | 66 ++++++++++------------------------------ c/moq/README.md | 2 +- c/moq/cmake/version.h.in | 8 +++++ c/moq/include/moq/moq.h | 5 +++ c/moq/test/probe.c | 61 +++++++++++++++++++++++++------------ rs/moq-ffi/MIGRATE.md | 5 --- 7 files changed, 76 insertions(+), 75 deletions(-) create mode 100644 c/moq/cmake/version.h.in diff --git a/c/moq/CMakeLists.txt b/c/moq/CMakeLists.txt index 5c98c143498..c2afdeaed74 100644 --- a/c/moq/CMakeLists.txt +++ b/c/moq/CMakeLists.txt @@ -19,6 +19,9 @@ find_program(UNIFFI_BINDGEN_CPP uniffi-bindgen-cpp REQUIRED) # uses the staticlib. Rendered to a scratch directory and copied only when changed, so a no-op # cargo build does not recompile the bindings. set(_generated "${CMAKE_CURRENT_BINARY_DIR}/include/moq/ffi") +# MOQ_IMPLEMENTATION names this package's version, which lives in VERSION. +set(_version_header "${CMAKE_CURRENT_BINARY_DIR}/include/moq/version.h") +configure_file(cmake/version.h.in "${_version_header}" @ONLY) set(_generated_files moq.h moq_c.cpp moq.hpp moq.cpp moq_scaffolding.hpp uniffi_expected.hpp) list(TRANSFORM _generated_files PREPEND "${_generated}/" OUTPUT_VARIABLE _generated_paths) set(_render "${CMAKE_CURRENT_BINARY_DIR}/render") @@ -90,6 +93,7 @@ if(PROJECT_IS_TOP_LEVEL) install(TARGETS moq-c ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(FILES include/moq/moq.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/moq) install(FILES "${_generated}/moq.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/moq/ffi) + install(FILES "${_version_header}" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/moq) # Renamed, so moq-cpp, which ships its own moq-ffi at its own pin, can install to the same # prefix without either overwriting the other's archive. set(MOQ_FFI_LIB_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}moq_c_ffi${CMAKE_STATIC_LIBRARY_SUFFIX}") diff --git a/c/moq/MIGRATE.md b/c/moq/MIGRATE.md index 1074f1f3fc4..cfa3a5b8eab 100644 --- a/c/moq/MIGRATE.md +++ b/c/moq/MIGRATE.md @@ -2,60 +2,25 @@ How to update your code for each breaking change, newest first. -## Unreleased - -### The C API is generated from moq-ffi (#6052) - -moq-c 0.8.0 replaces the hand-written ABI with one generated from moq-ffi, the crate every other binding wraps, so a function here means what its method means in Python, Go, or C++. -Every call changes shape once; the names are moq-ffi's in snake case, and `` documents each one. - -| 0.7 | 0.8 | -| --- | --- | -| `#include ` | `#include ` | -| `-lmoq` | `-lmoq -lmoq_c_ffi` plus the C++ runtime; take them from `pkg-config --libs moq-c` or `moq::c` | -| `int32_t` handles | opaque pointers, each released with its `moq__free` | -| a negative return code, then `moq_error()` | a returned `moq_error *` (NULL on success) with `tag`, its fields, and `moq_error_message()`; free it with `moq_error_free` | -| results written to `dst` | results written to the last argument, `out` | -| `const char *` plus a length | a NUL-terminated UTF-8 string | -| a zeroed config struct | `moq__default()`, then set fields | -| a value plus a `*_present` flag | `{ has_value, value }`, or a pointer that is NULL when absent | -| one `moq_status_callback` per stream of results | a callback per call that returns a `moq_task *`; call again for the next result, and `moq_task_free` cancels | -| callbacks on moq's callback thread | callbacks on moq's dispatcher thread, or your own loop through `moq_set_dispatcher` and `moq_work_run` | -| process teardown | `moq_shutdown()` before unloading; pending callbacks may run during it, never after | - -A few common calls: - -| 0.7 | 0.8 | -| --- | --- | -| `moq_origin_create` | `moq_origin_producer_new` | -| `moq_origin_create_broadcast` | `moq_origin_producer_create_broadcast` | -| `moq_publish_track` | `moq_broadcast_producer_publish_track` | -| `moq_session_connect` | `moq_client_new`, then `moq_client_connect` | -| `moq_session_failure` | `moq_client_failure` while the first dial retries, then `moq_session_failure`; each returns the last failure as a `moq_error *`, or NULL | -| `moq_server_listen` | `moq_server_new`, `moq_server_listen`, then `moq_server_accept` per session | -| `moq_consume_track` | `moq_broadcast_consumer_subscribe_track`, then `moq_track_consumer_read_frame` per frame | +## moq-c 0.7, the hand-written ABI -```c -static void on_frame(void *user_data, moq_error *error, moq_frame *frame) { - if (error) { - fprintf(stderr, "read failed: %s\n", moq_error_message(error)); - moq_error_free(error); - } else if (frame) { - use(frame->payload.data, frame->payload.len); - moq_frame_free(frame); - } -} +The hand-written crate never published as moq-c: a libmoq 0.6 user applies these entries before moving to the generated API. -moq_task *task = moq_track_consumer_read_frame(track, on_frame, user_data); -// Free it after the callback, or before to cancel the read. -``` +### Sessions report a lifecycle snapshot -The archive holds the compiled C API (`libmoq.a` / `moq.lib`) and moq-ffi's staticlib, installed as `libmoq_c_ffi.a` / `moq_c_ffi.lib` so it never overwrites moq-cpp's, and links the C++ runtime. -It embeds the same Rust runtime as the C++ package, `moq-cpp`, so a program links one of the two, never both. +`moq_session_connect` and `moq_session_request_accept` take a `moq_state_callback` instead of a `moq_status_callback`: `void on_state(void *user_data, const moq_connection_state *state)`. +Each change delivers a `moq_connection_state` with `phase`, `connects`, `failure`, and the live `protocol`; copy what you keep, since it is valid only during the callback. +Read `state->connects` where you read a positive status code. A failed dial or dropped connection now fires the callback with a negative `failure`, so drop your `moq_session_failure` polling; `moq_error()` describes it during the callback. +The terminal call has phase `MOQ_CONNECTION_PHASE_CLOSED`, with `failure` zero after a requested close or the error that ended the session, where the terminal status code used to be. Release `user_data` there, as before. -## moq-c 0.7, the hand-written ABI +### Exact encoder rates and publication options -The hand-written crate never published as moq-c: a libmoq 0.6 user applies these entries, then the table above. +`moq_video_encoder_input.framerate` is now `moq_video_frame_rate { numerator, +denominator }`, changing the C ABI layout. Replace `30` with `{30, 1}` or use +`{30000, 1001}` for an exact fractional rate. Both components must be nonzero; +rates above 1,000,000 fps are refused. Recompile callers with the generated header +and matching native library. Invalid rational rates return `MOQ_ERROR_INVALID_CONFIG`. +Audio and publication track arguments are unchanged. ### Video rendition preference @@ -98,7 +63,7 @@ Clamp timestamps you build from unbounded input to `2^61 - 1`. ### Finished tracks stay until the broadcast ends (#6157) -`moq_publish_track_finish`, `moq_publish_media_finish`, `moq_publish_container_finish`, and the encoder finishes now release the handle while the broadcast keeps the finished track listed, with its catalog rendition and demand, until `moq_publish_close` or the new `moq_publish_finish_close`. +`moq_publish_track_finish`, `moq_publish_media_finish`, `moq_publish_container_finish`, and the encoder finishes now release the handle while the broadcast keeps the finished track listed, with its catalog rendition and demand, until `moq_publish_close`. Publish a replacement under a new name, and expect a finished track's demand terminal callback when the broadcast closes rather than at the finish. ### A claim answers by publishing (#5678) @@ -236,6 +201,7 @@ if (frame.timestamp_present) use(frame.timestamp_us); ### `moq_publish_finish` is removed (#4152) Close the broadcast with `moq_publish_close`. +To end its content first, call `moq_publish_catalog_finish`; the broadcast stays up until `moq_publish_close`. ### A decoded frame can fail on its own (#4094) diff --git a/c/moq/README.md b/c/moq/README.md index f7c290a3f8a..7b22c0f366c 100644 --- a/c/moq/README.md +++ b/c/moq/README.md @@ -1,6 +1,6 @@ # moq C package -The C API for [rs/moq-ffi](../../rs/moq-ffi), generated by the C backend of `uniffi-bindgen-cpp`, plus `include/moq/moq.h`, which adds the one call the generator cannot: `moq_shutdown`. Nothing here re-implements a function; a shape problem is fixed in moq-ffi, where every binding benefits. User docs: [doc/lib/c](../../doc/lib/c/index.md). Migrating from the hand-written 0.7 ABI: [MIGRATE.md](MIGRATE.md). +The C API for [rs/moq-ffi](../../rs/moq-ffi), generated by the C backend of `uniffi-bindgen-cpp`, plus `include/moq/moq.h`, which adds the one call the generator cannot: `moq_shutdown`. Nothing here re-implements a function; a shape problem is fixed in moq-ffi, where every binding benefits. User docs: [doc/lib/c](../../doc/lib/c/index.md). Migration guide: [MIGRATE.md](MIGRATE.md). ## Layout diff --git a/c/moq/cmake/version.h.in b/c/moq/cmake/version.h.in new file mode 100644 index 00000000000..ede8ad4b3b6 --- /dev/null +++ b/c/moq/cmake/version.h.in @@ -0,0 +1,8 @@ +/* Generated by CMake from c/moq/VERSION. */ +#ifndef MOQ_VERSION_H +#define MOQ_VERSION_H + +/* The moq C package version. */ +#define MOQ_C_VERSION "@PROJECT_VERSION@" + +#endif diff --git a/c/moq/include/moq/moq.h b/c/moq/include/moq/moq.h index 52284402872..5863546741c 100644 --- a/c/moq/include/moq/moq.h +++ b/c/moq/include/moq/moq.h @@ -3,6 +3,11 @@ #define MOQ_MOQ_H #include +#include + +/* This package's product token. Every config is generated, so nothing sets it for you: put it in + * moq_client_config.implementation and moq_server_config.implementation, after your own tokens. */ +#define MOQ_IMPLEMENTATION "moq-c/" MOQ_C_VERSION #ifdef __cplusplus extern "C" { diff --git a/c/moq/test/probe.c b/c/moq/test/probe.c index f962b9b9c96..154f2a4e7d9 100644 --- a/c/moq/test/probe.c +++ b/c/moq/test/probe.c @@ -1,7 +1,7 @@ /* Exercises the installed package end to end over a real QUIC session from plain C: a returned - * error, a loopback connect, a frame read through a callback, a cancelled read, a retrying - * first dial's failure, a callback in flight at shutdown, and every callback run on this - * thread through a custom dispatcher. */ + * error, a loopback connect naming the package, a frame read through a callback, a cancelled + * read, a retrying first dial's state, a callback in flight at shutdown, and every callback run + * on this thread through a custom dispatcher. */ /* clock_gettime and the pthread timed wait under strict C11. */ #define _POSIX_C_SOURCE 200809L @@ -221,6 +221,20 @@ static void *await(result *pending, const char *what) { return pending->value; } +static void on_state(void *user_data, moq_error *error, moq_connection_state *value) { + on_value(user_data, error, value); +} + +/* Waits for the state after `state`, which it frees. */ +static moq_connection_state *next_state(moq_session *session, moq_connection_state *state) { + result changed = {0}; + moq_task *task = moq_session_state_changed(session, state->revision, on_state, &changed); + moq_connection_state *next = await(&changed, "state_changed"); + moq_task_free(task); + moq_connection_state_free(state); + return next; +} + int main(void) { #ifdef _WIN32 InitializeSRWLock(&loop.lock); @@ -272,6 +286,7 @@ int main(void) { /* Both halves of the handshake are pending at once, and complete on this thread. */ moq_client_config client_config = moq_client_config_default(); client_config.tls.insecure = true; + client_config.implementation = MOQ_IMPLEMENTATION; moq_client *client = NULL; ok(moq_client_new(&client_config, &client), "client new"); char url[128]; @@ -283,6 +298,10 @@ int main(void) { moq_task *connect_task = moq_client_connect(client, url, on_session, &connecting); moq_request *request = await(&accepting, "accept"); CHECK(request); + /* The package names itself ahead of moq-ffi, so a server can tell bindings apart. */ + char *implementation = moq_request_peer_implementation(request); + CHECK(implementation && strncmp(implementation, "moq-c/", 6) == 0); + moq_string_free(implementation); result serving = {0}; moq_task *serve_task = moq_request_accept(request, NULL, NULL, on_session, &serving); moq_session *served = await(&serving, "request accept"); @@ -367,30 +386,34 @@ int main(void) { moq_broadcast_producer_free(broadcast); moq_origin_producer_free(origin); - /* A first dial that keeps failing never resolves connect, so moq_client_failure reports - * its last failure; the server is gone, so every dial fails. */ + /* A first dial that keeps failing still returns the session, whose state names each failed + * attempt while it retries, then ends closed once cancelled; the server is gone, so every + * dial fails. */ moq_client_config retry_config = moq_client_config_default(); - retry_config.timeout_us.has_value = true; - retry_config.timeout_us.value = 100000; + retry_config.timeout.has_value = true; + retry_config.timeout.value = 100000000; /* 100ms */ retry_config.websocket.enabled.has_value = true; retry_config.websocket.enabled.value = false; moq_client *retrying = NULL; ok(moq_client_new(&retry_config, &retrying), "client new"); - CHECK(moq_client_failure(retrying) == NULL); result dialing = {0}; task = moq_client_connect(retrying, url, on_session, &dialing); - moq_error *failure = NULL; - for (int waited = 0; !failure && waited < 5000; waited += 10) { - CHECK(!pump(&dialing.done, 10)); - failure = moq_client_failure(retrying); - } - CHECK(failure && failure->tag == MOQ_ERROR_CONNECT); - moq_error_free(failure); - moq_client_cancel(retrying); - CHECK(pump(&dialing.done, 5000)); - CHECK(dialing.error && dialing.error->tag == MOQ_ERROR_CANCELLED); - moq_error_free(dialing.error); + moq_session *redialing = await(&dialing, "retrying connect"); moq_task_free(task); + moq_connection_state *state = moq_session_state(redialing); + while (!state->failure) { + state = next_state(redialing, state); + } + CHECK(state->phase == MOQ_CONNECTION_PHASE_CONNECTING); + CHECK(state->failure->tag == MOQ_ERROR_CONNECT); + CHECK(state->connects == 0); + moq_session_cancel(redialing, 0); + while (state->phase != MOQ_CONNECTION_PHASE_CLOSED) { + state = next_state(redialing, state); + } + CHECK(state->connects == 0); + moq_connection_state_free(state); + moq_session_free(redialing); moq_client_free(retrying); /* A read whose frame arrives just before shutdown has its callback in flight, so it may run diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index ab94547efe9..4e987c81481 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -5,11 +5,6 @@ These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in it ## Unreleased -### `MoqAudioEncoderFinish` is `MoqAudioEncoderFinished` (#6052) - -The record `MoqAudioEncoder::finish` returns is renamed, so its C name no longer collides with the method's. -The wrappers' `EncoderFinish` aliases are unchanged. - ### Durations are typed Every duration is a UniFFI `Duration` instead of an integer microsecond field, and drops its `_us` suffix: each language receives its own duration type (`timedelta`, `TimeInterval`, `java.time.Duration`, `time.Duration`, Dart `Duration`, C++ `std::chrono` nanoseconds). From 92788c3cf9a0f7536a5448fb4a320bff00f9d5da Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 17:10:51 -0700 Subject: [PATCH 25/25] ci(c): install the generator pin that renders typed durations Co-Authored-By: Claude Opus 5.5 --- .github/workflows/c.yml | 2 +- .github/workflows/release-c.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/c.yml b/.github/workflows/c.yml index b46892169f0..484bd5b8698 100644 --- a/.github/workflows/c.yml +++ b/.github/workflows/c.yml @@ -36,7 +36,7 @@ jobs: - uses: ./.github/actions/just - name: Install uniffi-bindgen-cpp - run: cargo install uniffi-bindgen-cpp --locked --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.5+v0.32.2 + run: cargo install uniffi-bindgen-cpp --locked --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.7+v0.32.2 - name: Check run: just c check diff --git a/.github/workflows/release-c.yml b/.github/workflows/release-c.yml index a16314ab5ba..7ec826f4cb2 100644 --- a/.github/workflows/release-c.yml +++ b/.github/workflows/release-c.yml @@ -60,7 +60,7 @@ jobs: # The tag flake.nix pins; see the comment there. - name: Install uniffi-bindgen-cpp shell: bash - run: cargo install uniffi-bindgen-cpp --locked --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.5+v0.32.2 + run: cargo install uniffi-bindgen-cpp --locked --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.7+v0.32.2 # The package version lives in c/moq/VERSION; a tag that disagrees is a mistake. - name: Check version