Skip to content

feat(connect-cpp): add the C++17 convenience layer and surface the C/C++ channel - #107

Merged
auto-wood merged 33 commits into
mainfrom
feat/cpp-consumer-surface
Sep 18, 2026
Merged

auto-wood merged 33 commits into
mainfrom
feat/cpp-consumer-surface

Conversation

@auto-wood

Copy link
Copy Markdown
Contributor

What

Adds the C++17 consumer surface for the Connect C/C++ channel, and makes that channel discoverable from the
entry documentation a C++ integrator actually reads.

Convenience layer — crates/spoke-connect/bindings/cpp/include/spoke_connect.hpp: a hand-written,
header-only C++17 wrapper over the existing C ABI (spoke_connect.h, ABI revision 1 unchanged). Move-only
owning types for the C resources, a Result<T> / Error error channel, std::string_view text inputs with an
explicit owned copy, std::optional for the C optional-value convention, callback records built from
std::function and transferred through unique_ptr only after the C constructor succeeds, explicit close()
on sessions (destruction frees but never closes), and an exception-containment branch selected from the compiler
exception macros. No new exported symbol, no record-layout change, no new artifact or version-bearing manifest.

Verification — the smoke runner compiles and runs two configurations per RID (exceptions disabled and
enabled) and requires each configuration's assertion banners to match its expected list exactly, so a
banner leaked from the other configuration fails the run; the C ABI symbol/layout/callback gate's C++ probe now
also compiles the wrapper and gained a .hpp exception-syntax negative control; the Windows CI lane runs the
same gate with --self-test so the negative mutations execute on MSVC too.

Documentation — the C++ how-to pages (EN + CN) carry a runnable walkthrough composed of three copyable
blocks that reaches Established, invokes a tool and closes in the documented order on both the success and the
failure paths; the C/C++ channel is surfaced on the root READMEs, the docs home and the package quick-start; one
authoritative registry-backed / git-based channel classification replaced the three drifting counts across
CONCEPTS.md, AGENTS.md and the specs; and the two tracked C++-related specs state the shipped facts.

Verification

  • node tooling/connect/cpp-symbol-check.mjs --header … --library … --self-test → 83 declarations, 83 exports, 0 missing, 0 extra; layout, callback-signature, C and C++ probes green; all four negative mutations failing
    as intended
  • node tooling/connect/cpp-smoke.mjs --rid osx-arm64 → both configurations green with their exact ordered
    banners
  • node tooling/docs/twin-parity.mjs → every page twinned, heading structures matching
  • pnpm docs:build green; node tooling/docs/deadlink-check.mjs → 49 pages, 2 761 anchors, 0 dead
  • The how-to walkthrough was extracted from the page's own blocks, built and run under both clang exception
    configurations, observing Established, a non-empty session id and the echo JSON
  • Plan QC tri-review Approve with residuals; QA gate mandatory → Approve, 10/10 acceptance criteria met

Residuals

  • R1 (low) — the symbol gate's C++ inclusion probe covers only the no-exceptions configuration; the
    exception-enabled path is covered by the smoke runner. Tracking: project register entries["cpp-consumer-surface"].
  • R2 (low) — the committed C++ carrier provenance records a header hash for spoke_connect.h only; a
    spoke_connect.hpp fingerprint can only land at the next carrier refresh. Tracking: the same register bucket.

No blocking finding and no critical remains open.

Notes

  • The committed carriers for osx-arm64 and win-x64 are unchanged; the Windows lane's run is CI-owned and was
    not executed locally.
  • A linux-x64 C carrier is recorded as a deferred roadmap item with owner, trigger and done definition; it is
    not implemented here.
  • The other languages' binding READMEs keep their pre-existing internal spec links; only the C++ consumer surface
    this change owns was repointed to public destinations.

README twins: name C and C++ in the Connect capability bullet, add a
tag-checkout passage with both headers and the two committed carriers,
and replace the four-publish-channels sentence with the registry-backed
/ git-based classification.

Docs home twins: name C/C++ in the how-to feature copy and add the
dedicated C++ how-to link beside the TypeScript / native shortcuts.

Quick-start twins: add the "C and C++ (git)" subsection under Connect
with the tag clone, the header and carrier paths, and the committed
platform sentence.
…T and docs

Replace the four/five channel counts with the registry-backed / git-based
grouping everywhere the connect native-binding channels are described, and
record the C/C++ channel in the internal SSOT.

- connect-publish-strategy.md carries the authoritative classification
  statement plus the registry-backed and git-based channel tables, and
  gains C/C++ rows in the surface inventory, staging, owner class, auth,
  disposition, non-goals, roadmap pointer and links.
- spoke-connect.md Path B embedding row no longer describes only the
  uniffi route and lists the C/C++ committed headers and carriers.
- AGENTS.md, CONCEPTS.md and the publishing knowledge doc/index row use
  the same enumeration and meaning.
- connect-native-bindings how-to (EN + CN) states the classification, adds
  .hpp to the C/C++ row and gains a C and C++ - git subsection with the
  platform sentence and a link to the full walkthrough.
- explanation/connect twins note the C ABI + C++17 convenience
  relationship and the callback-transport pointer.
…list

The C and C++ subsection listed the win-x64 DLL but omitted the committed Rust-produced import library the Windows link step consumes. List crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll.lib in both the EN page and its zh twin so the page's acquisition paths cover the two headers and both carried Windows artifacts. Fixes review finding T2-2 on docs/packages/quick-start.md and docs/zh/packages/quick-start.md.
…s and loopback

`bindings/cpp/include/spoke_connect.hpp` is a header-only C++17 layer over the
hand-written C ABI in namespace `spoke::connect`: a move-only `Buffer` and
`Error`/`Result<T>`/`Result<void>`, the eight core free functions, the three
core session objects (`NonceStore`, `OutboundSequence`, `InboundSequence`) and
the loopback pair and ends, over a reusable move-only handle base. Every
production call reaches only an existing declaration in `spoke_connect.h`; the
carrier and the ABI revision 1 stay untouched.

The smoke gains a second translation unit (`Smoke/convenience.cpp`) that shares
`Smoke/support.hpp`'s golden read and parse, so both units include the
convenience header and one linked program covers single-header ODR.
`cpp-smoke.mjs` compiles and links both units and requires the new ordered
banner before the final one; the `cpp-symbol-check.mjs` C++ probe now includes
`.h` and `.hpp` (twice) and instantiates `Result`, `Buffer` and a move-only
handle, so the second header cannot stop compiling behind a green C-only check.
The plan's frozen consumer configuration (A2) fixes MSVC as
`/std:c++17 /EHs-c- /GR- /MD /D_HAS_EXCEPTIONS=0`; the smoke runner
compiles on behalf of a consumer, so its Windows argv must carry the
whole configuration. The clang argv already matched A2 and is unchanged.
`spoke_connect.hpp` gains the host-callback side of the C ABI: the three
callback records (`TransportCallbacks`, `PortsCallbacks` with all thirteen
slots named as the C table, `ToolCallbacks`), their complete static thunk
tables, the `Transport` / `PortsHandler` / `ToolHandler` factories — which
clear the caller's `unique_ptr` only after the C constructor succeeds — and
the `RemoteAdapter` / `ConnectResponder` wrappers over the frozen connection
and serving methods: baseline ports, `project` / `compute` /
`list_fork_timeline_events` / `extract`, `invoke_tool`,
`register_tool_handler`, the state / session / manifest accessors and explicit
`close`. No method exists that the production C surface does not have; the C
header, the carrier, the committed natives and ABI revision 1 are untouched.

The bridges follow the frozen error contract: a non-empty result moves into
host-owned storage released exactly once, a zero-length result uses a zero
record (the carrier releases only populated buffers), a present empty
diagnostic takes the address of a static empty string, and packaging several
error fields holds them in local RAII until the record is handed over.
Exception-enabled builds contain an escaping host exception — from the
callback and from the packaging that follows it — into
`SPOKE_CONNECT_TRANSPORT_IO` or `SPOKE_CONNECT_FFI_REJECTED` with
`INTERNAL_ERROR` / `callback` from static literals; with exceptions disabled
the header contains no `try`, `catch` or `throw` at all, and the same calls
report failure through `Result`.

`Smoke/convenience.cpp` proves the group over a host queue transport:
`serve` → `connect` → `Established`, a real baseline ports round trip with the
absent-versus-present-zero base revision, a tool round trip in both directions
plus the reverse registration path, a refusal preserving `code`, `message`,
`kind` and `wire_code`, a present empty `kind` that stays present, a refused
factory that leaves the caller its context, a transport handle released after
the session cloned it, and every callback record released exactly once after
close. `cpp-smoke.mjs` builds each RID twice — exceptions disabled (the
consumer default) and exceptions enabled — each against its own ordered banner
list, with the enabled build additionally reporting the containment row.

`cpp-symbol-check.mjs` runs its probes through a build step the negative
controls reuse, adds the `.hpp` exception-syntax mutation over a complete copy
of the header tree (so an include-path mistake cannot pass as a control), and
its C++ probe now instantiates the callback records, the three factories and
the two session wrappers.
The banner verifier scanned for each expected banner in order and recorded
only the ones it could not find, so a run that printed extra or repeated
banners still passed: the exceptions-disabled build could report the enabled
configuration's containment banner, or a group could run twice, and the gate
stayed green.

Extract every banner-shaped line the run printed and require that sequence to
equal the configuration's expected sequence — same count, same order, same
text — failing with the first differing position when a banner is missing,
extra, repeated or reordered.
…+ coverage

Wrap the router group of the frozen C surface in `spoke::connect`: a
move-only `MultiPeerRouter` with `create`, register/unregister/list peers,
the composed manifest, the routed baseline ports and `invoke_tool`. The
production C surface has no router extract/project/compute/fork member, so
none is invented, and registration borrows the adapter — the destructor
releases the router's own references and never closes a caller-owned
adapter. The loopback ends stay independent helpers; no end is presented as
a callback `Transport`.

Prove it in the smoke with a live session: register an established adapter,
route a real tool call through the router, unregister the peer and observe
the terminal `no_capable_peer` reject with zero wire traffic, then show the
released router leaving the caller-owned adapter Established and serving.
The group prints `C++ convenience router: PASS` before `C++ smoke: PASS` in
both configurations.

Close out the coverage statement: `parity.md` gains a C++ counterpart
column for every production entry — business API versus RAII mechanics —
and the symbol gate's C++ probe now instantiates the value layer, all
eleven opaque handles and the three callback factories under the consumer
no-exception flags.
…le walkthrough

The C++ page leads with `spoke_connect.hpp`: three consecutive blocks compose
one complete `host.cpp` (host queue transport, demo identity with a served echo
tool, dial plus one real invoke) that builds and runs against the committed
carrier, followed by the `Buffer`/`Result`/close semantics and where a real
network transport replaces the callbacks. The acquisition table, Windows
no-exception flags, smoke banners (two configurations per RID) and the symbol
gate's inclusion-probe label are synced to the shipped artifacts.

The tracked specs record the layer as a current fact: coverage, values, the
result and exception contract, callback bridges and explicit close in
`connect-cpp-binding.md`, with `.hpp` folded into the header gate and the
convenience exclusions removed; the channels spec drops the convenience
non-goal and states the builder's closed two-target set. The binding, UE and
crate READMEs follow, with the golden-vector sentence no longer counting
bindings.
The crate README (published on crates.io) carried five `../../.mstar/specs/`
pointers that do not resolve for its audience and expose the internal
agent-facing SSOT. Route the wire facts to the on-site Connect wire reference
(page and section anchors) instead.

The C++ binding and UE READMEs each pointed at the internal C++ binding
decision record; the consumer how-to and the C++ binding README already carry
those facts, so drop the decision-record references and keep the sentences.

No technical content changes; link destinations only.
The Windows lane invoked cpp-symbol-check.mjs without --self-test, so the four negative mutations (missing declaration, invented declaration, callback-signature drift, .hpp exception-syntax probe) never ran on MSVC even though the plan's B2 gate and the documented delivery recipe include the flag on both platforms. The self-test only reuses tooling the step already has (cl.exe and dumpbin) and adds compile time, so the flag belongs on this lane too.
…se path

The composed `host.cpp` terminated through `std::exit` on failure paths that
follow a successful `ConnectResponder::serve()` — the tool handler, the tool
registration and the dialer transport — and on a failed dial in `main`. Those
paths bypassed both automatic-object destruction and the explicit
`RemoteAdapter::close()` / `ConnectResponder::close()` / host-queue-close
sequence the same page documents, which the plan's C4 freeze requires a
post-establish failure to use.

`start_endpoints` now returns `Result<Endpoints>` and routes every step after
`serve` through one `close_ordered` helper that ends the sessions in the
documented order and closes the host queues before the failure travels back to
`main`; a failed dial and a failed `RemoteAdapter::state()` use the same helper,
which the success tail now also calls. `unwrap` stays for the steps that run
before a serving handle exists; its `Result<void>` overload became unused and is
removed.

The happy path and its stdout are unchanged: the page's own macOS command
builds and runs the concatenated blocks and observes the same `state:
Established` line, session id and echo JSON. EN and CN keep the C++ source
byte-identical.
@cursor
cursor Bot requested a review from btspoony September 18, 2026 09:45

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Left a non-blocking comment and assigned reviewers. This PR adds a large C++ FFI convenience layer with transport/callback bridges, which is above the medium approval threshold, so it needs human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

Comment thread tooling/connect/cpp-symbol-check.mjs Dismissed
Comment thread crates/spoke-connect/bindings/cpp/Smoke/support.hpp Fixed

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Left a non-blocking comment; one reviewer is already assigned. This C++ FFI convenience layer (transport and callback bridges) is above the medium approval threshold, so it needs human review. Cursor Bugbot and Cursor Security Agent were not present after the first check poll.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@greptile-apps

greptile-apps Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no outstanding correctness or repository-rule violations identified.

Summary

The PR adds a header-only C++17 convenience layer over the existing C ABI, expands smoke and symbol validation across exception configurations and supported carriers, and documents the C/C++ distribution channel.

  • Introduces move-only C++ resource wrappers, structured results, optional-value handling, callback bridges, and explicit session closure.
  • Runs exact-banner smoke validation with exceptions enabled and disabled.
  • Adds macOS carrier validation and ensures native-carrier changes trigger both pull-request and push workflows.
  • Updates English and Chinese integration documentation, binding specifications, and channel classification.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    CPP["C++17 consumer"] --> HPP["spoke_connect.hpp<br/>header-only convenience layer"]
    HPP --> H["spoke_connect.h<br/>C ABI revision 1"]
    H --> LIB["Committed platform carrier"]
    LIB --> RUST["spoke-connect Rust facade"]
    CHECK["Symbol and layout gate"] --> H
    CHECK --> HPP
    CHECK --> LIB
    SMOKE["Exception-disabled and<br/>exception-enabled smoke"] --> HPP
    SMOKE --> LIB
Loading

Reviews (5) · Last reviewed commit: "Merge branch 'feat/cpp-consumer-surface-..."

Comment thread tooling/connect/cpp-smoke.mjs
`bannerLines` split the child's output on "\n" and tested every line against
/^.+: PASS$/. The Windows smoke child writes CRLF, so each line kept a trailing
"\r" and nothing matched: the windows-smoke job failed reporting 9 expected, 0
printed, from a run that had printed all nine banners and passed every
assertion. Splitting on /\r?\n/ consumes CRLF and LF alike as a single line
terminator, so the extraction no longer depends on the platform's line ending.

The check itself is unchanged: the observed banners must still equal the
configuration's expected list exactly — same count, same order, same text — and
the diagnostic still names the first differing position with both counts.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Left a non-blocking comment; one reviewer is already assigned. This C++ FFI convenience layer with transport and callback bridges is above the medium approval threshold, so it needs human review. Cursor Bugbot and Cursor Security Agent were not present after the first check poll.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

greptile-apps[bot]
greptile-apps Bot previously approved these changes Sep 18, 2026
…ed in path expression'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@greptile-apps
greptile-apps Bot dismissed their stale review September 18, 2026 11:13

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Left a non-blocking comment; one reviewer is already assigned. This C++ FFI convenience layer with transport and callback bridges is above the medium approval threshold, so it needs human review. Cursor Bugbot and Cursor Security Agent were not present after the first check poll.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

greptile-apps[bot]
greptile-apps Bot previously approved these changes Sep 18, 2026
…helper

Copilot Autofix 4beb3d6 added resolve_fixture_path() to Smoke/main.cpp and spelled the report label unqualified; kFixtureLabel is declared in the spoke_smoke namespace by Smoke/support.hpp, so the three references did not compile.

Import it with a using-declaration beside the file's other spoke_smoke imports (banner, check, fail, Golden, json_string_field, load_golden, read_file), matching how this translation unit already refers to that namespace.

The helper's behaviour is unchanged: the fixture path is still canonicalised and confined to crates/spoke-connect/tests/fixtures.
@greptile-apps
greptile-apps Bot dismissed their stale review September 18, 2026 11:22

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Left a non-blocking comment; reviewers were not newly assigned because one reviewer is already requested. This C++ FFI convenience layer with transport and callback bridges is above the medium approval threshold, so it needs human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

Comment thread .github/workflows/cpp-connect.yml

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: high. Left a non-blocking comment; reviewers were not newly assigned because one reviewer is already requested. This C++ FFI convenience layer with transport and callback bridges is above the medium approval threshold, so it needs human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@btspoony
btspoony enabled auto-merge (squash) September 18, 2026 11:31
@btspoony
btspoony disabled auto-merge September 18, 2026 11:45
@auto-wood
auto-wood merged commit 56e0ab1 into main Sep 18, 2026
22 checks passed
@auto-wood
auto-wood deleted the feat/cpp-consumer-surface branch September 18, 2026 11:46
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.

3 participants