zerobus-sdk / cpp
CLAUDE.md94 starsChanged 10 days agoApache-2.0View on GitHub
C++ wrapper around the Rust core via the C FFI library (rust/ffi/zerobus.h). Like Go, it consumes the C FFI directly; unlike Go there is no GC, so resource management is RAII and errors are reported by throwing zerobus::ZerobusException. The public headers never include zerobus.h; they hold opaque, forward-declared FFI handles. Only src/ includes the generated C header, keeping the C FFI an implementation detail of the SDK. Run from cpp/: - make build — Configure (CMake) and build the SDK,…
In this repository6 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- go/CLAUDE.mdCLAUDE.md
- java/CLAUDE.mdCLAUDE.md
- python/CLAUDE.mdCLAUDE.md
- rust/CLAUDE.mdCLAUDE.md
- typescript/CLAUDE.mdCLAUDE.md
The file
We split this CLAUDE.md into the rules it gives an agent
8 rules. About 0 are common, 0 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 94
When using a custom HeadersProvider:
- Very specific: line 116
When a StreamOptions::ack_callback is set:
- Very specific: line 177
Every FFI crossing serializes data.
- Very specific: line 187
Every PR must update cpp/NEXT_CHANGELOG.md under the appropriate section if it changes user-facing behavior.
- Very specific: line 189
Update cpp/README.md if the change affects usage, setup, or API surface.
- Very specific: line 190
Add or update examples in cpp/examples/ for new or modified APIs.
- Very specific: line 191
Add doc comments (///) for all new public items.
- Very specific: line 205
Create the cpp/v<semver> tag.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
C++ SDK
C++ wrapper around the Rust core via the C FFI library (rust/ffi/zerobus.h).
Like Go, it consumes the C FFI directly; unlike Go there is no GC, so resource
management is RAII and errors are reported by throwing zerobus::ZerobusException.
Structure
cpp/
├── include/zerobus/ # Public headers (the API surface)
│ ├── zerobus.hpp # Umbrella header — include this
│ ├── sdk.hpp # Sdk + SdkBuilder + TableProperties
│ ├── stream.hpp # Stream (proto/JSON ingestion)
│ ├── arrow_stream.hpp # ArrowStream (Arrow Flight)
│ ├── proto_schema.hpp # ProtoSchema (UC JSON → descriptor + encoder)
│ ├── headers_provider.hpp # HeadersProvider interface
│ ├── ack_callback.hpp # AckCallback interface + AckCallback::from adapter
│ ├── config.hpp # StreamOptions / ArrowStreamOptions / CallbackWaitPolicy / enums
│ ├── record.hpp # UnackedRecord
│ ├── error.hpp # ZerobusException
│ └── version.hpp # ZEROBUS_CPP_VERSION
├── src/ # Implementation (the only place that includes zerobus.h)
│ ├── sdk.cpp / stream.cpp / arrow_stream.cpp / proto_schema.cpp
│ ├── ack_callback.cpp # AckCallback::from lambda adapter
│ ├── headers_callback.cpp # extern "C" trampoline for HeadersProvider
│ └── detail/ # Internal: ffi_util, config_convert, headers_callback, ack_callback
├── tests/ # Dependency-free unit tests (plain executables)
├── examples/ # Runnable usage examples (JSON, proto, Arrow)
├── cmake/
│ ├── BuildRustFfi.cmake # Builds libzerobus_ffi from local Rust source
│ └── zerobus-config.cmake.in # find_package(zerobus) package-config template
├── CMakeLists.txt
└── Makefile # Convenience wrappers (build/test/lint/fmt)The public headers never include zerobus.h; they hold opaque, forward-declared
FFI handles. Only src/ includes the generated C header, keeping the C FFI an
implementation detail of the SDK.
Build commands
Run from cpp/:
make build— Configure (CMake) and build the SDK, tests, and the examples underexamples/(gated onZEROBUS_BUILD_EXAMPLES, on by default)make build-ffi— Build only the Rust C FFI static librarymake test— Build and run the test suite (ctest)make lint— Formatting check + compiler warnings (-Wall -Wextra)make fmt— Format all sources with clang-format (Google style)make clean— Remove the build directorymake test SANITIZE=address— Build + run the suite under a sanitizer (address,thread, orundefined; CMake option-DZEROBUS_SANITIZE=). Off by default. Targets the memory/lifetime bugs this FFI wrapper is prone to (use-after-free, double-free).address/threadalso instrument the Rust FFI (nightly-Zsanitizer+-Zbuild-std) — a sanitizer only observes code compiled with it, so leaving the core uninstrumented would miss the races/UAF that live inside the FFI. This needs a nightly toolchain withrust-srcand build the C++ side with clang to match the LLVM sanitizer runtime (CLANG_CC/CLANG_CXX, defaultclang/clang++); GCC can't resolve the instrumented archive's runtime symbols.undefineduses the default compiler and a plain FFI build. Sanitizer builds use their own build dir (build-<sanitizer>), so they don't clash with a normalbuild/(CMake caches the compiler per build dir).
CMake builds the FFI library from local Rust source by default
(cargo build --release in rust/ffi). To link a prebuilt/vendored library
instead, configure with -DZEROBUS_FFI_LIBRARY=<path-to-.a> and
-DZEROBUS_FFI_HEADER_DIR=<dir>.
FFI boundary: RAII + static linking
Memory ownership
- Opaque handles: each wrapper class holds a raw pointer to a Rust-allocated
handle (
CZerobusSdk,CZerobusStream,CArrowStream,CZerobusProtoSchema). Rust owns the memory. - RAII cleanup: destructors call the matching free/close function
(
zerobus_sdk_free,zerobus_stream_close+zerobus_stream_free,zerobus_arrow_stream_close+zerobus_arrow_stream_free, etc.). Wrapper objects are move-only (no copies) — moving nulls the source handle so it is freed exactly once. - Error strings: FFI calls route their
CResultthrough a helper indetail/ffi_util.hpp, which converts failure into aZerobusExceptionand always freesCResult.error_messageviazerobus_free_error_message. - Returned arrays:
Stream::get_unacked_records/ArrowStream::get_unacked_batchescopy the payloads out, then free the FFI array (zerobus_free_record_array/zerobus_arrow_free_batch_array).ProtoSchema::encode_jsonfrees its buffer withzerobus_free_proto_bytes;descriptor_bytes()copies the borrowed bytes.
Headers provider callback
When using a custom HeadersProvider:
detail::zerobus_cpp_headers_trampolineis anextern "C"function passed tozerobus_sdk_create_stream_with_headers_provider.user_datais a heap-allocatedstd::shared_ptr<HeadersProvider>*whose ownership is handed to the FFI, alongsidedetail::zerobus_cpp_headers_freeas the FFI's providerfree_user_datadestroy callback.- The FFI invokes
zerobus_cpp_headers_freeexactly once, on every path (on success after the core's last reference drops — i.e. after any in-flightget_headers()has returned; on a failed create before returning). This is what closes the recovery-vs-teardown use-after-free: a slowget_headers()racing stream teardown can no longer run on a freed provider. Because the FFI owns the provider, theStream/ArrowStreamno longer keeps ashared_ptr<HeadersProvider>member, and the wrapper must not freeuser_dataitself (doing so would double-free). - The trampoline marshals the returned map into
CHeadersusing the allocators the Rust core'szerobus_free_headersexpects to free. Do not change the allocator without checkingzerobus_free_headersin the FFI crate. - Exceptions thrown from
get_headers()are caught and surfaced as a headers-provider error; they never cross the FFI boundary as C++ exceptions.
Ack callback
When a StreamOptions::ack_callback is set:
detail::zerobus_cpp_ack_on_ack_trampoline/zerobus_cpp_ack_on_error_trampolineare theextern "C"functions wired into the C config (ack_on_ack/ack_on_error);user_datais the rawAckCallback*.to_c()installs them only when the callback is non-null and zeroes all three fields otherwise (seedetail/config_convert.hpp).- The
Streamkeeps astd::shared_ptr<AckCallback>alive for its whole lifetime, but that is not always sufficient: a callback still running whenclose()hits its wait budget (StreamOptions::callback_wait_policy) can be invoked on a freed object once theStreamis destroyed.CallbackWaitPolicy:: forever()is the only policy that guarantees no callback runs afterclose()returns. This is the canonical FFI-side view of the contract ininclude/zerobus/ack_callback.hpp— keep the two in sync. AckCallback::on_ack/on_errorarenoexcept. The trampolines must not let anything unwind across the boundary; an escaping exception callsstd::terminateat the throw site.
Configuration mapping
detail::to_c (overloaded for StreamOptions and ArrowStreamOptions) seeds
the C config struct from the live FFI defaults (zerobus_get_default_config /
zerobus_arrow_get_default_config) and then overrides each known field. The
Stream variant expresses optional fields (e.g. stream_paused_max_wait_time_ms)
with the C struct's has_* presence flags; the Arrow variant uses a -1
sentinel for "wait the full server-specified pause duration". config_defaults_test
and arrow_config_defaults_test fail the build if the hand-kept scalar defaults
in StreamOptions / ArrowStreamOptions drift from the FFI defaults.
StreamOptions::callback_wait_policy (a CallbackWaitPolicy) also maps to a
has_*/value pair: forever() clears has_callback_max_wait_time_ms so Rust
reads None (drain indefinitely), duration(ms) sets an explicit finite budget,
and use_default() leaves the FFI seed in place. The ack-callback trampolines
and ack_user_data are set from StreamOptions::ack_callback (see Ack
callback).
Thread safety
- A
Stream/ArrowStreamis not safe for concurrent use — serialize access externally (same contract as the Rust core). - A
HeadersProvider::get_headers()may be invoked from an internal worker thread; the core guards against overlapping calls but implementations should be thread-safe with respect to their own state. - A single
ProtoSchemamay be used by concurrent readers (descriptor_bytes,encode_json); destruction must not race those calls.tests/concurrency_test.cppexercises this under ThreadSanitizer (which instruments the Rust core too), so a race in the read path is caught in CI. The destruction-vs-read contract, though, is not tested — it is the caller's responsibility, not something the TSan job enforces.
Breaking change rules
Public API is everything under include/zerobus/:
- Removing/renaming public types, methods, or struct fields, or changing a method signature, is breaking.
- Adding new methods, new overloads, or new struct fields with defaults is safe.
- Deprecate with
[[deprecated("Use X instead")]]and keep for at least one minor release before removal in the next major (semver, tagscpp/v*).
Performance notes
- Every FFI crossing serializes data; prefer the batch APIs
(
ingest_proto_records,ingest_json_records) over per-record calls in hot paths. - Ingestion is asynchronous:
ingest_*queues and returns. Never wait per record (wait_for_offset/flushin the loop); flush once at the end or flush periodically. Examples and doc comments must follow this pattern. - The static FFI library is linked at build time — no runtime loading overhead.
Changelog and documentation
- Every PR must update
cpp/NEXT_CHANGELOG.mdunder the appropriate section if it changes user-facing behavior. - Update
cpp/README.mdif the change affects usage, setup, or API surface. - Add or update examples in
cpp/examples/for new or modified APIs. - Add doc comments (
///) for all new public items.
Release
The distribution model is CMake + GitHub Releases only — no package manager (no
Conan, vcpkg, etc.), mirroring the C FFI. The C++ wrapper is always built from
the consumer's own compiler (its C++ ABI is not stable across compiler/stdlib),
so we do not ship a prebuilt libzerobus_cpp. What we ship prebuilt is the
Rust C FFI archive (libzerobus_ffi.a) — a stable C ABI, and the one piece a
consumer can't build without a Rust toolchain. A consumer then either builds
everything from source (needs Rust) via add_subdirectory / FetchContent /
find_package, or points -DZEROBUS_FFI_LIBRARY= at the matching prebuilt FFI
archive from a release and builds only the C++ (no Rust needed).
- Create the
cpp/v<semver>tag. - Manually dispatch the
release-zerobus-sdk-cpp.ymlorchestrator (insecure-public-registry-releases-eng) with the tag asref. It invokesrelease-cpp.ymlin this repo to build the per-platform bundles (the Rust C FFI archive cross-built withcargo-zigbuild, plus the platform-neutral C++ source tree), scans the artifacts, and cuts the GitHub Release with a per-platformtar.gzattached and notes fromcpp/CHANGELOG.md. - Version source:
cpp/include/zerobus/version.hpp(ZEROBUS_CPP_VERSION) and theproject(... VERSION ...)line incpp/CMakeLists.txt— keep them in sync (CMake fails configuration if they disagree). The release validates the tag version againstZEROBUS_CPP_VERSION. - The C++ SDK links the FFI static library. If Rust FFI code changed, an FFI
release (
ffi/v*) must happen first; for source builds the workspace must be present. - On version bump PR: move
NEXT_CHANGELOG.mdcontents toCHANGELOG.mdunder a## Release v<semver>heading (the release notes are extracted by matching that heading), resetNEXT_CHANGELOG.md.
Config / requirements
- C++17 compiler (GCC, Clang, or MSVC)
- CMake ≥ 3.16
- Rust toolchain required for building the FFI from source
- Tests are dependency-free: each is a plain executable that returns
non-zero on failure and is registered with CTest (see
tests/CMakeLists.txt), so the build is hermetic — no vendored framework, no package manager, no network. Tests reach into the privatesrc/detailheaders, so each links the library target and addssrc/to its include path. If real third-party C++ test dependencies are ever needed, that's the point to revisit the tooling — not before.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

