diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml new file mode 100644 index 0000000000..c43d3fa746 --- /dev/null +++ b/.github/workflows/cpp.yml @@ -0,0 +1,58 @@ +name: C++ + +# Compiles the generated moq-ffi C++ bindings with clang, outside Nix. `just +# check` already runs `just cpp 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`. MSVC runs in nightly.yml, for the reason the Windows Rust check +# does: those runners are throttled too hard to gate every pull request. +# +# 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: + - "cpp/ffi/**" + - "cpp/justfile" + - "rs/moq-ffi/**" + - "Cargo.lock" + - ".github/workflows/cpp.yml" + +concurrency: + group: cpp-${{ 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 + + # Pinned: `--locked` fixes just's own dependencies, not which version of + # just cargo selects. + - name: Install just + run: cargo install --locked just@1.52.0 + + - 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.1+v0.32.2 + + - name: Check + run: just cpp check + env: + CXX: clang++ diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 9f73f46109..58a4071fa7 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -295,6 +295,43 @@ jobs: shell: bash run: just rs windows + # The generated moq-ffi C++ bindings under MSVC, with exceptions and RTTI + # off. cpp.yml covers clang on pull requests and `just check` covers gcc; this + # is the Windows half, nightly for the same runner cost as `windows`. + cpp-windows: + name: C++ (MSVC) + runs-on: windows-latest + 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 + + # aws-lc-rs assembles its x86_64 crypto with NASM on Windows. + - name: Install NASM + shell: pwsh + run: | + choco install nasm -y --no-progress + "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Install just + shell: bash + run: cargo install --locked just@1.52.0 + + # 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.1+v0.32.2 + + - name: Check + shell: bash + run: just cpp check + # `just rs macos` is the only thing that compiles moq-video's VideoToolbox # encode/decode and its ScreenCaptureKit / AVFoundation capture, plus # moq-audio's ScreenCaptureKit system audio and its TCC permission pre-check. diff --git a/cpp/ffi/.gitignore b/cpp/ffi/.gitignore new file mode 100644 index 0000000000..c5190f8227 --- /dev/null +++ b/cpp/ffi/.gitignore @@ -0,0 +1,3 @@ +# Regenerated from rs/moq-ffi by `just cpp check`. +/generated/ +/build/ diff --git a/cpp/ffi/CMakeLists.txt b/cpp/ffi/CMakeLists.txt new file mode 100644 index 0000000000..7c87906d53 --- /dev/null +++ b/cpp/ffi/CMakeLists.txt @@ -0,0 +1,34 @@ +# Compiles the generated moq-ffi bindings and the probe that exercises them. +# `just cpp check` generates the bindings and drives this; it is not a package. +cmake_minimum_required(VERSION 3.16) +project(moq-ffi-probe LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# The cargo output directory holding the moq-ffi cdylib. +set(MOQ_FFI_LIB_DIR "" CACHE PATH "Directory containing the moq-ffi shared library") +if(NOT MOQ_FFI_LIB_DIR) + message(FATAL_ERROR "Set MOQ_FFI_LIB_DIR to the cargo output directory") +endif() + +set(GENERATED ${CMAKE_CURRENT_SOURCE_DIR}/generated) +find_package(Threads REQUIRED) + +add_executable(probe probe.cpp ${GENERATED}/moq.cpp) +target_include_directories(probe PRIVATE ${GENERATED}) + +if(MSVC) + # No exceptions and no RTTI, the way Unreal builds. + target_compile_options(probe PRIVATE /permissive- /EHs-c- /GR-) + target_compile_definitions(probe PRIVATE _HAS_EXCEPTIONS=0) + target_link_libraries(probe PRIVATE ${MOQ_FFI_LIB_DIR}/moq_ffi.dll.lib) + add_custom_command(TARGET probe POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${MOQ_FFI_LIB_DIR}/moq_ffi.dll $) +else() + target_compile_options(probe PRIVATE -Wall -Wextra -Werror -pedantic-errors -fno-exceptions -fno-rtti) + find_library(MOQ_FFI moq_ffi PATHS ${MOQ_FFI_LIB_DIR} NO_DEFAULT_PATH REQUIRED) + target_link_libraries(probe PRIVATE ${MOQ_FFI} Threads::Threads) + set_target_properties(probe PROPERTIES BUILD_RPATH ${MOQ_FFI_LIB_DIR}) +endif() diff --git a/cpp/ffi/README.md b/cpp/ffi/README.md new file mode 100644 index 0000000000..0b2e996249 --- /dev/null +++ b/cpp/ffi/README.md @@ -0,0 +1,32 @@ +# C++ bindings for moq-ffi + +C++17 bindings for [rs/moq-ffi](../../rs/moq-ffi), generated by `uniffi-bindgen-cpp`. This directory holds the generator config and a probe program; the generated sources are not checked in. The ergonomic `moq::` layer and the package that ships them come later. + +## Generating + +`just cpp check` builds moq-ffi for the host, regenerates `generated/` (gitignored), then compiles and runs `probe.cpp` against it. The probe connects over QUIC, subscribes to a track, reads a frame through a future, cancels a pending read, and checks errors come back as values. It builds with exceptions and RTTI disabled. + +The generator is a fork, [kixelated/uniffi-bindgen-cpp](https://github.com/kixelated/uniffi-bindgen-cpp). It carries LiveKit's async support ported to uniffi 0.32, plus the `error_style = "expected"` option this directory's `uniffi.toml` turns on. The dev shell provides it. Without Nix: + +```bash +cargo install uniffi-bindgen-cpp --locked \ + --git https://github.com/kixelated/uniffi-bindgen-cpp \ + --tag v0.11.0-kixelated.1+v0.32.2 +``` + +`flake.nix` pins the same tag and lists every other place that names it. + +## Shape + +Every generated type lives in `namespace moq`, spelled as in Rust (`moq::MoqClient`). Objects are `std::shared_ptr`; records and enums are values. + +- A fallible call returns `uniffi::expected`. `uniffi::expected` is `std::expected` on C++23 and a bundled `tl::expected` below it. Build the generated sources with the same standard as the code that includes them. +- An async call returns `uniffi::Future`. Block on it with `get()` or `wait_for()`, or attach a continuation with `std::move(future).then(executor, callback)`, which returns a `uniffi::FutureContinuation` handle. +- `moq::MoqError` is a value holding a `std::variant` of its cases. Nothing throws. A Rust panic or a misused future aborts with a message on stderr. +- Continuations run on one process-wide dispatcher thread unless the application installs its own with `uniffi::set_async_dispatcher` before the first async call. Call `uniffi::shutdown_async_dispatcher()` before unloading the code a continuation could call into. It stops dispatch and abandons every pending future. + +## Cancellation + +Cancelling a future (`cancel()`, destroying it, or destroying the `FutureContinuation` returned by `then`) drops the Rust future. Native moq-ffi runs each async call as a spawned task that holds an `AbortOnDrop` on it (`rs/moq-ffi/src/ffi.rs`), so dropping the future aborts the work at its next await point instead of letting it finish unobserved. The continuation of a cancelled future never runs, and `get()` on it aborts. + +The abort discards the operation, not the handle it ran on, so the next call on that object works. Where a method keeps partial progress, its doc comment says so. For example, a cancelled `read_frame` leaves the current group for the next read. Every write in moq-ffi (`write_frame`, `finish`, `abort`) is synchronous, so cancelling a future can never tear a write. Only the async operations (reads, subscribes, connects, accepts, and `reject`) can be cut short. diff --git a/cpp/ffi/probe.cpp b/cpp/ffi/probe.cpp new file mode 100644 index 0000000000..feda812adb --- /dev/null +++ b/cpp/ffi/probe.cpp @@ -0,0 +1,125 @@ +// Exercises the generated C++ bindings end to end over a real QUIC session: connect, +// subscribe, read a frame through a future, cancel a pending read, and observe errors +// as returned values. Built with exceptions and RTTI disabled. + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +// Unlike assert, this survives a release build. +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #expr); \ + std::abort(); \ + } \ + } while (0) + +namespace { + +[[noreturn]] void fail(const char *what, const moq::MoqError &error) { + std::fprintf(stderr, "%s failed: MoqError variant %zu\n", what, error.get_variant().index()); + std::abort(); +} + +// Unwraps a result the probe expects to succeed. +template +T ok(uniffi::expected result, const char *what) { + if (!result) { + fail(what, result.error()); + } + return std::move(*result); +} + +void ok(uniffi::expected result, const char *what) { + if (!result) { + fail(what, result.error()); + } +} + +std::vector bytes(const std::string &text) { + return std::vector(text.begin(), text.end()); +} + +} // namespace + +int main() { + // A synchronous error is a returned value, not an exception. + auto unbound = moq::MoqServer::init(); + auto fingerprints = unbound->cert_fingerprints(); + CHECK(!fingerprints); + CHECK(std::holds_alternative(fingerprints.error().get_variant())); + + // Publisher: a server whose origin serves one broadcast with one track. + auto origin = moq::MoqOriginProducer::init({}); + auto broadcast = ok(origin->create_broadcast("probe"), "create_broadcast"); + auto track = ok(broadcast->publish_track("data", std::nullopt), "publish_track"); + ok(broadcast->announce({}), "announce"); + + auto server = moq::MoqServer::init(); + ok(server->set_bind("127.0.0.1:0"), "set_bind"); + ok(server->set_tls_generate({"localhost"}), "set_tls_generate"); + ok(server->set_publish(origin), "set_publish"); + auto addr = ok(server->listen().get(), "listen"); + + // Both halves of the handshake are futures, so they run concurrently while this + // thread blocks on one at a time. + auto client = moq::MoqClient::init(); + ok(client->set_tls_verify(false), "set_tls_verify"); + auto accepting = server->accept(); + auto connecting = client->connect("https://" + addr); + auto request = ok(accepting.get(), "accept"); + CHECK(request != nullptr); + auto served = ok(request->accept().get(), "request accept"); + auto session = ok(connecting.get(), "connect"); + + // Subscriber: resolve the announced broadcast and subscribe to its track. + auto announced = ok(session->consume()->announced_broadcast("probe"), "announced_broadcast"); + auto remote = ok(announced->available().get(), "available"); + auto consumer = ok(remote->subscribe_track("data", std::nullopt).get(), "subscribe_track"); + + // Cancel a read that has nothing to deliver yet. + auto pending = consumer->read_frame(); + CHECK(pending.wait_for(50ms) == std::future_status::timeout); + pending.cancel(); + CHECK(!pending.valid()); + + // The consumer survives the cancelled read and delivers the next frame. + auto reading = consumer->read_frame(); + ok(track->write_frame({bytes("hello"), 1000}), "write_frame"); + auto frame = ok(reading.get(), "read_frame"); + CHECK(frame.has_value()); + CHECK(frame->payload == bytes("hello")); + CHECK(frame->timestamp_us == 1000); + + // An async error arrives through the future as a returned value too: this track + // finished, so reading past its end on a second, finished consumer reports it. + ok(track->finish(), "track finish"); + auto ended = consumer->read_frame().get(); + CHECK(ended && !ended->has_value()); + + auto closed = moq::MoqClient::init(); + closed->cancel(); + auto refused = closed->connect("https://" + addr).get(); + CHECK(!refused); + CHECK(std::holds_alternative(refused.error().get_variant())); + + ok(broadcast->finish(), "broadcast finish"); + session->cancel(0); + served->cancel(0); + server->cancel(); + + // Stop dispatching continuations before the process tears down. + uniffi::shutdown_async_dispatcher(); + + std::printf("probe: ok\n"); + return 0; +} diff --git a/cpp/ffi/uniffi.toml b/cpp/ffi/uniffi.toml new file mode 100644 index 0000000000..3dccc7caa8 --- /dev/null +++ b/cpp/ffi/uniffi.toml @@ -0,0 +1,4 @@ +# uniffi-bindgen-cpp config for rs/moq-ffi. Errors are returned, never thrown, so the +# bindings build with exceptions disabled (Unreal's default). +[bindings.cpp] +error_style = "expected" diff --git a/cpp/justfile b/cpp/justfile new file mode 100644 index 0000000000..efc2de2d50 --- /dev/null +++ b/cpp/justfile @@ -0,0 +1,51 @@ +#!/usr/bin/env just --justfile +# +# The generated C++ bindings for rs/moq-ffi. Invoked from the repo root as +# `just cpp ` via the `mod cpp` import. The OBS plugin has its own +# module, `just obs`. + +set working-directory := '.' + +default: + just check + +# Takes an optional newline-separated list of changed files and skips when none +# are in scope; `just cpp check` (no FILES) always runs. CXX picks the compiler +# on Unix; Windows uses MSVC. + +# Build moq-ffi, regenerate cpp/ffi/generated, then compile and run the probe. +check $FILES="": + #!/usr/bin/env bash + set -euo pipefail + if [[ -n "$FILES" ]] && ! grep -qE '^(cpp/ffi/|cpp/justfile$|rs/moq-ffi/)' <<< "$FILES"; then + echo "cpp: no C++ binding changes; skipping." + exit 0 + fi + if ! command -v uniffi-bindgen-cpp >/dev/null 2>&1; then + echo "cpp check: uniffi-bindgen-cpp not on PATH, skipping" >&2 + echo " install: cargo install uniffi-bindgen-cpp --locked --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.1+v0.32.2" >&2 + exit 0 + fi + + cargo build --locked --package moq-ffi + target_dir=$(cargo metadata --format-version 1 --no-deps | jq -r .target_directory)/debug + case "$(uname -s)" in + Darwin) cdylib="$target_dir/libmoq_ffi.dylib" ;; + MINGW* | MSYS* | CYGWIN*) cdylib="$target_dir/moq_ffi.dll" ;; + *) cdylib="$target_dir/libmoq_ffi.so" ;; + esac + + rm -rf ffi/generated + uniffi-bindgen-cpp --library "$cdylib" --config ffi/uniffi.toml --out-dir ffi/generated + + cmake -S ffi -B ffi/build -DMOQ_FFI_LIB_DIR="$target_dir" -DCMAKE_BUILD_TYPE=Debug + cmake --build ffi/build --config Debug + if [[ -x ffi/build/probe ]]; then + ffi/build/probe + else + ffi/build/Debug/probe.exe + fi + +# Remove the generated bindings and the probe build. +clean: + rm -rf ffi/generated ffi/build diff --git a/flake.nix b/flake.nix index 028422fd1b..1b3699d2aa 100644 --- a/flake.nix +++ b/flake.nix @@ -331,6 +331,49 @@ doCheck = false; }; + # uniffi-bindgen-cpp renders rs/moq-ffi into cpp/ffi/generated. Not in + # nixpkgs, so build it from source; without it `just cpp check` skips + # itself, 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. Five other places name the same tag and must be + # bumped together: the `cargo install` lines in rs/moq-ffi/build.sh, + # cpp/justfile, cpp/ffi/README.md, .github/workflows/cpp.yml, and the + # cpp-windows job in .github/workflows/nightly.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, + # and the fork adds `error_style = "expected"`, which cpp/ffi/uniffi.toml + # turns on. Its tags add a `-kixelated.N` pre-release so they never + # collide with upstream's. Move back upstream once one tags both. + uniffi-bindgen-cpp = pkgs.rustPlatform.buildRustPackage rec { + pname = "uniffi-bindgen-cpp"; + version = "0.11.0-kixelated.1+v0.32.2"; + + src = pkgs.fetchFromGitHub { + owner = "kixelated"; + repo = "uniffi-bindgen-cpp"; + rev = "v${version}"; + hash = "sha256-i5qVHviZS36TpmaWINNgLKx12cWPm0TUT9+k+YaNAvw="; + }; + + cargoHash = "sha256-+Vt69WTtR/evH+qPcI0J7I1OKWtVOo1xzoGD4Hwg3zE="; + + # The workspace's other member is the fixture crate, which pulls the + # uniffi examples in from git; build only the generator. + buildAndTestSubdir = "bindgen"; + + # The upstream tests generate fixtures and compile them with CMake, + # which is a lot of build for a binary we only invoke. + doCheck = false; + }; + + # C++ binding generator; CMake comes from rustDeps and the compiler from stdenv. + cppDeps = [ + uniffi-bindgen-cpp + ]; + # Dart bindings plus the pinned external generator. dartDeps = [ pkgs.dart @@ -444,7 +487,7 @@ moq-gst ; - inherit uniffi-bindgen-dart; + inherit uniffi-bindgen-cpp uniffi-bindgen-dart; # Bundle of packaging + repo-publish tooling, pinned via flake.lock. # CI builds this and prepends its bin/ to $PATH so subsequent steps @@ -479,6 +522,7 @@ ++ obsDeps ++ ktDeps ++ goDeps + ++ cppDeps ++ dartDeps ++ devTools; diff --git a/justfile b/justfile index ff1b2951cf..61adb3dca3 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,8 @@ mod kt mod swift mod go mod dart +# Generated C++ bindings for moq-ffi. +mod cpp # OBS Studio plugin (C++). See doc/bin/obs.md. mod obs 'cpp/obs' # Unit tests per language (`just test`). @@ -451,6 +453,7 @@ _tools $FILES="": # here to prevent. scoped '^(go/|rs/moq-ffi/)' && tools+=(go uniffi-bindgen-go cargo rsync) scoped '^(dart/|rs/moq-ffi/)' && tools+=(cargo dart uniffi_bindgen_dart) + scoped '^(cpp/ffi/|cpp/justfile$|rs/moq-ffi/)' && tools+=(cargo jq cmake c++ uniffi-bindgen-cpp) # Two obs recipes with two dispatch scopes, so two lines: over-requiring # would fail a diff that never runs the recipe. `just obs compile` needs # cargo to regenerate moq.h and pkg-config to locate Qt6 and ffmpeg. Every @@ -520,6 +523,7 @@ check $BASE="" *args: just swift check just go check just dart check + just cpp check just obs check just obs compile just _flake @@ -544,6 +548,7 @@ check $BASE="" *args: just swift check "$files" just go check "$files" just dart check "$files" + just cpp check "$files" # Type-checking the plugin and its unit tests needs only headers, so it # runs here rather than waiting for obs.yml to link them on Linux. libmoq # is in scope because the plugin calls through its generated C header, and diff --git a/quest/m1/cpp/README.md b/quest/m1/cpp/README.md index 3faa892c8f..cdb8b38282 100644 --- a/quest/m1/cpp/README.md +++ b/quest/m1/cpp/README.md @@ -31,11 +31,12 @@ Rust side carried across. Continuations run on a default bounded dispatcher unless the application installs its own (`uniffi::set_async_dispatcher`); OBS installs one that hops to its own threads. -Errors never throw. The fork gains an `error_style = expected` flag so every -generated method returns `moq::expected` and futures deliver the -same, which is what lets Unreal and other `-fno-exceptions` builds consume the -package. `moq::expected` is `std::expected` on C++23 and a bundled -`tl::expected` below it. +Errors never throw. The fork's `error_style = "expected"` flag makes every +fallible generated method return `uniffi::expected` and every +future deliver the same, which is what lets Unreal and other `-fno-exceptions` +builds consume the package. `uniffi::expected` is `std::expected` on C++23 and a +bundled `tl::expected` below it; the `moq::` layer renames both. Callback +interfaces are refused under that flag until one is needed. One library, C++17 floor (OBS's baseline), feature-gated extras: `co_await` on a future under `__cpp_impl_coroutine`, `std::expected` under @@ -49,7 +50,6 @@ reads the same release manifest so a release bumps both. ## Quests -- [Generator](/quest/m1/cpp/generator.md) - the uniffi 0.32 C++ generator with futures and expected-style errors, pinned and generating `cpp/ffi` in CI - [Package](/quest/m1/cpp/package.md) - the `cpp/moq` wrapper, CMake package, release tarball, interop client, and docs - [OBS migration](/quest/m1/cpp/obs.md) - the OBS plugin moves from libmoq handles and trampolines to the generated C++ diff --git a/quest/m1/cpp/generator.md b/quest/m1/cpp/generator.md deleted file mode 100644 index 19ac2fe6c2..0000000000 --- a/quest/m1/cpp/generator.md +++ /dev/null @@ -1,48 +0,0 @@ -# [M] uniffi-bindgen-cpp: futures and expected-style errors on uniffi 0.32 - -## Goal - -`just cpp check` regenerates `cpp/ffi` from `rs/moq-ffi` with a pinned C++ -generator and compiles a small program that connects, subscribes to a track, -reads one frame through a future, cancels a pending read, and observes an -error as a returned `expected`, on gcc, clang, and MSVC at C++17. This is the -go/no-go gate for the questline: if the generator cannot be made to hold, -stop here and write down why. - -## Plan - -- Fork `livekit/uniffi-bindgen-cpp` at branch `livekit/uniffi-0.31-async` - (futures with `get`/`wait_for`/`cancel`/`then`, async callback interfaces, - a pluggable dispatcher; last commits 2026-09-08) into - `kixelated/uniffi-bindgen-cpp`. Port it to uniffi 0.32.x: the metadata - encoding changed in 0.32 without a contract bump, so a 0.31 generator fails - to read a 0.32 cdylib at all; the Go port (`kixelated/uniffi-bindgen-go` - `v0.8.0+v0.32.0`) is the worked example. Tag `vX.Y.Z+v0.32.0`. -- Add an `error_style = expected` generator option: methods return - `moq::expected` instead of throwing, `uniffi::Future::get()` - returns the same, `then` continuations already receive a result type. Ship - a bundled `tl::expected` and alias `std::expected` when `__cpp_lib_expected` - is defined. Callback interfaces implemented in C++ return errors the same - way. The exceptions style stays the default so the fork remains upstreamable. -- Verify the generated headers compile with `-fno-exceptions` under the - expected style (Unreal's default), and that the async dispatcher shuts down - cleanly when the consumer unloads (`uniffi::shutdown_async_dispatcher`). -- Pin the fork in `flake.nix` next to `uniffi-bindgen-go` and `uniffi-bindgen-dart` - with the same comment discipline: every place that names the generator - version is listed and bumped together (`rs/moq-ffi/build.sh`, the release - workflow, `cpp/ffi/README.md`, `doc/lib/cpp`). `just cpp check` regenerates - into `cpp/ffi` and compiles the probe; wire it into the check workflow the way - `just go check` is. -- Document the cancellation contract from what moq-ffi does: native - `Task::run` and `detached` hold an `AbortOnDrop` on the spawned task - (`rs/moq-ffi/src/ffi.rs`), so dropping the Rust future aborts the work, and - a mid-write abort is possible. The generated future's `cancel()` and its - destructor map onto that; the wrapper says so where it matters (finish a - group before dropping its write future). -- Offer both the 0.32 port and the expected flag upstream to LiveKit and - NordSecurity; the fork exists only until they tag. - -## Related - -- [#2907](/quest/m4/2907-bind-the-browser-through-moq-ffi-uniffi-instead-of-a.md) - the browser generator spike; same Task and `#[cfg]`-inside-export gotchas apply -- [C# generator](/quest/m2/cs/generator.md) - the same 0.32 port against NordSecurity's C# generator diff --git a/quest/m1/cpp/package.md b/quest/m1/cpp/package.md index fa546c0e9c..c372c4831e 100644 --- a/quest/m1/cpp/package.md +++ b/quest/m1/cpp/package.md @@ -23,6 +23,10 @@ A `test/interop/clients/cpp` client joins `just test interop --all`, and - Executor: the default dispatcher is fine for scripts; document how a host installs its own (`moq::set_executor`) before the first call, and that continuations must not block on the moq-ffi runtime thread. +- Shutdown: `moq::shutdown()` calls `moq_ffi_shutdown` and + `uniffi::shutdown_async_dispatcher`, so a host that unloads (a plugin, an + engine module) releases the runtime thread's buffers; the generator probe + still reports about 72 bytes held at exit under valgrind without it. - Build: `cpp/CMakeLists.txt` builds `libmoq_ffi` (staticlib, features `video`+`audio`) with the same `cargo build` custom command and `BUILD_RUST_LIB` switch `rs/libmoq/CMakeLists.txt` uses (no Corrosion, so @@ -38,7 +42,3 @@ A `test/interop/clients/cpp` client joins `just test interop --all`, and they exist; the future, expected, executor, and coroutine rules) and a row in `doc/lib/index.md`. The C row now says libmoq is the plain-C ABI and points C++ readers at the new package. - -## Required - -- [Generator](/quest/m1/cpp/generator.md) - the pinned generator that emits `cpp/ffi` diff --git a/quest/m2/cs/generator.md b/quest/m2/cs/generator.md index 0966cbebb8..511d2eb5f1 100644 --- a/quest/m2/cs/generator.md +++ b/quest/m2/cs/generator.md @@ -20,7 +20,3 @@ catches a typed `MoqException`, on the host runtime. generator: a cancelled token drops the Rust future, and `IDisposable` on a handle cancels its pending calls. If upstream lacks the mapping, it lands in the fork here, never in the wrapper; the probe above is the acceptance test. - -## Related - -- [C++ generator](/quest/m1/cpp/generator.md) - the same port against the C++ generator diff --git a/rs/moq-ffi/build.sh b/rs/moq-ffi/build.sh index 75342c3f08..3c4e19b530 100755 --- a/rs/moq-ffi/build.sh +++ b/rs/moq-ffi/build.sh @@ -189,8 +189,20 @@ generate_bindings() { echo " Skipping dart bindings: uniffi_bindgen_dart not on PATH" fi + # C++ uses a third-party bindgen too: a fork carrying LiveKit's async support + # ported to uniffi 0.32, which upstream has not released. Install it with: + # cargo install --locked uniffi-bindgen-cpp --git https://github.com/kixelated/uniffi-bindgen-cpp --tag v0.11.0-kixelated.1+v0.32.2 + if command -v uniffi-bindgen-cpp >/dev/null 2>&1; then + echo " Generating cpp bindings..." + uniffi-bindgen-cpp --library "$lib_path" \ + --config "$WORKSPACE_DIR/cpp/ffi/uniffi.toml" \ + --out-dir "$OUTPUT_DIR/bindings/cpp" + else + echo " Skipping cpp bindings: uniffi-bindgen-cpp not on PATH" + fi + if [[ "$ARCHIVE" == true ]]; then - for lang in kotlin swift python go dart; do + for lang in kotlin swift python go dart cpp; do if [[ -d "$OUTPUT_DIR/bindings/$lang" ]]; then local archive="moq-ffi-${VERSION}-${lang}.tar.gz" tar -czf "$OUTPUT_DIR/$archive" -C "$OUTPUT_DIR/bindings" "$lang"