Skip to content

feat(cpp): generate moq-ffi C++ bindings with a pinned uniffi-bindgen-cpp - #4100

Merged
kixelated merged 3 commits into
quest/m1/cpp/READMEfrom
quest/m1/cpp/generator
Sep 25, 2026
Merged

kixelated merged 3 commits into
quest/m1/cpp/READMEfrom
quest/m1/cpp/generator

Conversation

@kixelated

@kixelated kixelated commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The cpp questline generates its C++ surface from moq-ffi instead of writing it by hand. That only works if a C++ generator can read a uniffi 0.32 library, gives cancellable futures, and reports errors without exceptions, which Unreal and other -fno-exceptions builds need. None of the upstream generators does all three. This quest is the go/no-go gate for the line.

Verdict: go. The generator holds.

Approach

Generator: kixelated/uniffi-bindgen-cpp, tag v0.11.0-kixelated.1+v0.32.2, forked from LiveKit's livekit/uniffi-0.31-async (their PR #1).

  • 0.32 port: bumped uniffi_bindgen to 0.32.2, loaded cargo metadata through CrateConfigSupplier's own constructor, rendered Box<T> as T, and refused HashSet<T>. It was small: two compile errors. All 25 upstream C++ tests pass on gcc, clang, and MSVC, and under valgrind.

  • error_style = "expected" (off by default, so exceptions stay the upstream default):

    • Fallible calls return uniffi::expected<T, E>, and async calls return uniffi::Future<T, E>, whose get() and then() deliver the same.
    • uniffi::expected is std::expected when __cpp_lib_expected is defined, otherwise a bundled tl::expected v1.1.0 (CC0).
    • Error enums are value types, and flat errors carry message.
    • With no error value to return, the bindings abort: panics, checksum mismatches, and misused futures.
    • A cancelled or dropped future is abandoned: its continuation never runs.
    • The generated code has no throw, try, or catch. A new fork test builds it with -fno-exceptions -fno-rtti (/EHs-c- /GR- on MSVC) and runs it on all three compilers plus valgrind. It covers typed errors, async errors, continuations, cancellation, and dispatcher shutdown.
  • Fixed three existing LiveKit bugs that the fork's CI exposed:

    • A ready Rust future freed without rust_future_complete leaked its result. This happens when the dispatcher rejects its continuation, including after shutdown.
    • A foreign future that Rust dropped never called its completion callback, which leaked the oneshot sender.
    • The generated header lacked #include <string>, which MSVC needs.

    Each fix has a regression test that fails without it.

This repo:

  • cpp/ffi/: holds uniffi.toml (expected style), a CMake file, and probe.cpp. The probe compiles with exceptions and RTTI off and runs over a real QUIC session. It:

    • connects a client to an in-process server
    • subscribes to a track
    • reads a frame through a future
    • cancels a pending read and shows the consumer still delivers afterwards
    • gets errors back as values, both sync (kBind) and async (kCancelled)
    • shuts the dispatcher down

    The generated sources are gitignored, like Go's.

  • just cpp check builds moq-ffi, regenerates the bindings, and builds and runs the probe. It runs in just check (gcc in the dev shell), cpp.yml (clang, on PRs touching cpp/ffi or rs/moq-ffi), and a new cpp-windows nightly job (MSVC).

  • flake.nix pins the fork next to uniffi-bindgen-go. The comment lists every place that names the tag. rs/moq-ffi/build.sh emits bindings/cpp when the generator is on PATH.

  • cpp/ffi/README.md documents the cancellation contract. Native Task::run holds an AbortOnDrop, so cancelling or destroying a future aborts the work at its next await point. The handle survives, and the next call works. Every moq-ffi write is synchronous, so a cancel cannot tear a write. That correction retires the quest's "finish a group before dropping its write future" worry.

  • Deleted quest/m1/cpp/generator.md and every reference to it. The line README's plan now describes what the generator actually emits.

Impact

  • No wire change. No Rust public API change.
  • New, unpublished C++ surface: every moq-ffi export as moq::Moq*, returning uniffi::expected<T, moq::MoqError> and uniffi::Future<T, moq::MoqError>. The moq:: renames and the moq::expected alias belong to the Package quest.
  • New dev-shell tool and flake package: uniffi-bindgen-cpp. MOQ_STRICT now requires it (plus cmake, c++, jq) for diffs touching cpp/ffi or rs/moq-ffi.
  • New CI: cpp.yml (clang, PR), and cpp-windows in nightly.yml.

Decisions (maintainer's call; I picked the recommended option)

  1. Base: LiveKit PR Improve readme #1 (uniffi-0.31-async), as the quest planned. Alternative: PR Bump golang.org/x/net from 0.0.0-20220421235706-1d1ef9303861 to 0.7.0 in /cert #5 (uniffi-0.31-cpp17-async, on livekit/dev). It is newer, but it runs a worker thread per in-flight future and has no then() or async callback interfaces, which the OBS migration needs.
  2. Tag v0.11.0-kixelated.1+v0.32.2 follows the Dart fork's pre-release convention, so it can never collide with an upstream tag. Alternative: plain v0.11.0+v0.32.2, like the Go fork.
  3. Cancellation under expected abandons rather than errors. A generic E has no cancelled variant. Alternative: deliver expected<T, std::variant<E, Cancelled>>, which is noisier on every call site.
  4. Callback interfaces are refused under expected (generation fails). moq-ffi has none. Their foreign-future bridge is built on std::exception_ptr, and redesigning it waits for a consumer.
  5. MSVC runs nightly, not per PR, matching the Windows Rust check and its runner-cost rationale. Alternative: add a Windows leg to cpp.yml.

Verification

  • Local MOQ_STRICT=1 just check in the dev shell: just cpp check passes with the flake-pinned generator. So do the lints, just obs check, and nix flake check. The one failure is just obs compile: target/include/moq.h missing. libmoq's build script derives the header path from OUT_DIR, which my local build cache relocates. This PR does not touch libmoq or the plugin.
  • Local just test: 4852 passed.
  • Fork CI (gcc, clang, MSVC on Linux, macOS, Windows): green, including expected-test with exceptions off and the valgrind runs.

Follow-ups

  • Upstream offer: left to the maintainer. Nothing was posted to LiveKit or NordSecurity. Candidates for LiveKit PR Improve readme #1 (or NordSecurity once they take async): the 0.32 port, error_style = "expected", and the two leak fixes plus the <string> include.
  • The moq probe has not run under MSVC yet. The fork's MSVC job covers the generator and the expected style with exceptions off. The first cpp-windows nightly is the first MSVC build of the moq probe and just cpp check's Windows paths.
  • Valgrind reports about 72 bytes of QUIC send buffers still owned by moq-ffi's runtime thread at probe exit, because nothing calls moq_ffi_shutdown from C++. The Package quest's moq:: layer is the natural place to expose it.
  • doc/lib/cpp is not created. The Package quest owns the docs page, and flake.nix names the places to bump when it lands.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits September 24, 2026 18:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-cpp

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review September 25, 2026 04:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T04:37:27.487432Z ee10b56 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Pushed a quest-only commit to this branch as part of the spawn-quests follow-up planning (see #4105), so the follow-up rides with the PR that surfaced it. No code changes.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 8c64aab into quest/m1/cpp/README Sep 25, 2026
34 checks passed
@kixelated
kixelated deleted the quest/m1/cpp/generator branch September 25, 2026 07:48
@kixelated

Copy link
Copy Markdown
Collaborator Author

Merged into quest/m1/cpp/README after all 30 checks passed on 99f447b. Codex reviewed ee10b56 with no findings; the later commit only touches a quest doc. Before merging, I synced the line branch with main via a merge commit (c00d37d), with no conflicts.

(Written by Claude Opus 5.5)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant