Skip to content

feat(marmot-c): add C bindings for the Marmot runtime#410

Closed
agent-p1p wants to merge 3 commits into
masterfrom
pip/darkmatter-138
Closed

feat(marmot-c): add C bindings for the Marmot runtime#410
agent-p1p wants to merge 3 commits into
masterfrom
pip/darkmatter-138

Conversation

@agent-p1p

@agent-p1p agent-p1p commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a marmot-c crate exposing the Marmot app runtime through a stable, minimal C ABI, so consumers that cannot pull in a UniFFI runtime — embedded targets, C/C++ apps, and FFI from languages without UniFFI support (Zig, Nim, Go, Lua, raw FFI) — can drive the same MarmotApp + MarmotAppRuntime pair that the Swift/Kotlin bindings use. This is the C-surface counterpart to marmot-uniffi, bringing the org's existing mdk-c shape to Dark Matter.

Design

The UniFFI surface is ~70 async methods plus live subscription objects. Mirroring every one as a typed extern "C" function would be a large, churn-prone ABI. Instead this crate keeps a fixed, tiny symbol set and marshals structured request/response bodies as JSON:

  • marmot_c_open / marmot_c_free — construct/destroy an opaque MarmotC * handle (owns its own tokio runtime).
  • marmot_c_start / marmot_c_shutdown / marmot_c_is_stopping — runtime lifecycle.
  • marmot_c_call(method, request_json) -> response_json — single command entrypoint.
  • marmot_c_string_free — release returned char *.

New runtime capabilities are added as dispatch methods (src/dispatch.rs), not new exported functions, so the ABI stays stable as the runtime grows. Async calls are driven synchronously via block_on on the handle's owned tokio runtime. Most runtime records already derive serde::Serialize, so response bodies are their canonical JSON.

First-cut method coverage

account/session (account.list|create_identity|login|remove), group operations (group.create|list|members|mls_state|invite_members|remove_members), message send/receive (message.send_text|list), agent text streams (agent_stream.start), and storage primitives (timeline.list, chat.list).

Live subscriptions (the UniFFI *Subscription next() objects) are intentionally not in this first cut — a callback/poll bridge is a natural follow-up, tracked in #411. Noted in the README.

Deliverables

  • cdylib + staticlib build targets.
  • Hand-curated include/marmot.h (cross-checked against cbindgen --lang c src/lib.rs; signatures + constants match).
  • pkg-config template (marmot-c.pc.in) and CMake/linking notes in the README.
  • AGENTS.md + CLAUDE.md symlink per repo convention; registered in the root AGENTS.md map.

Test Plan

  • cargo fmt --all --check — clean
  • RUSTFLAGS='-D warnings' cargo check --workspace --all-targets --locked (and --all-features) — clean
  • cargo clippy --workspace --all-targets --locked -- -D warnings (and --all-features) — clean
  • cargo test -p marmot-c --locked — 4 FFI tests pass (open/call/free, unknown-method, null-handle rejection, invalid-argument). Tests install a mock keyring (keyring_core::mock) so the real constructor path runs headless, mirroring marmot-uniffi's smoke test.

Notes

  • No changes to existing crates beyond workspace registration (Cargo.toml, Cargo.lock) and the root AGENTS.md map entry.
  • The crate construction mirrors marmot_uniffi::Marmot::new (keychain-backed account home) so the C surface tracks the UniFFI surface.

Part of #138. This PR delivers the request/response C ABI surface; the live
subscription surface (UniFFI's watch_agent_text_stream and the other
*Subscription objects) is deferred to a tracked follow-up, #411, so #138
stays open until that lands.


Open in Stage

Summary by CodeRabbit

Release Notes

  • New Features

    • Added C language bindings enabling C/C++ applications to integrate with the Marmot runtime via a stable, minimal API.
  • Documentation

    • New comprehensive documentation for C bindings, including usage examples, memory management guidelines, and build instructions.

Add a marmot-c crate exposing the Marmot app runtime through a stable,
minimal C ABI for consumers that cannot use UniFFI (embedded targets,
C/C++, and FFI from Zig/Nim/Go/Lua/etc.).

- Opaque MarmotC handle owning its own tokio runtime; async runtime
  calls driven synchronously via block_on.
- Single JSON command entrypoint (marmot_c_call) over a fixed symbol
  set, so new runtime methods are additive dispatch arms rather than ABI
  changes. First cut covers account/session, group ops, message
  send/receive, agent-stream anchor, and timeline/chat-list storage
  reads.
- cdylib + staticlib targets; hand-curated include/marmot.h (verified
  against cbindgen), pkg-config template, and packaging/linking notes in
  the README.
- FFI smoke tests (mock keyring) for open/call/free, unknown-method,
  null-handle rejection, and invalid-argument paths.

Closes #138
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@agent-p1p, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 30 minutes and 59 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd522302-2fc0-4e59-9b2b-be1426301a80

📥 Commits

Reviewing files that changed from the base of the PR and between e2dea2e and d5ab658.

📒 Files selected for processing (1)
  • crates/marmot-c/src/dispatch.rs

Walkthrough

Adds a new crates/marmot-c Rust crate to the workspace that exposes the Marmot app runtime through a stable, minimal C ABI. The crate produces cdylib/staticlib artifacts, defines an opaque MarmotC handle with lifecycle and JSON-dispatch entrypoints, includes a public C header, pkg-config template, and accompanying documentation.

Changes

marmot-c C bindings crate

Layer / File(s) Summary
Workspace registration and crate manifest
Cargo.toml, crates/marmot-c/Cargo.toml
Registers crates/marmot-c in the Cargo workspace and defines the crate manifest with cdylib, staticlib, and lib crate types plus all runtime and dev dependencies.
C ABI public header and pkg-config template
crates/marmot-c/include/marmot.h, crates/marmot-c/marmot-c.pc.in
Declares all stable ABI symbols in marmot.h: five status-code macros, the opaque MarmotC typedef, and seven exported function signatures with ownership and threading documentation. Adds the pkg-config .pc.in template for downstream linking.
MarmotC handle and MarmotCError types
crates/marmot-c/src/runtime.rs
Defines MarmotCError with numeric status-code mapping and From conversions from AppError/serde_json::Error. Defines MarmotC owning MarmotApp, MarmotAppRuntime, and a Tokio runtime; implements open, start, shutdown, and is_stopping using block_on.
JSON dispatch layer: DTOs and method routing
crates/marmot-c/src/dispatch.rs
Implements dispatch(kit, method, request_json) with serde DTOs for all supported operations, helper functions for relay URL parsing and hex group-id decoding, and match arms routing account, group, message, timeline, chat, and agent-stream method strings to runtime calls.
extern C entrypoints, FFI helpers, and unit tests
crates/marmot-c/src/lib.rs
Implements all seven extern "C" functions with null-pointer and UTF-8 validation via internal FFI helpers, and unit tests covering mock-keyring open, known-method call, unknown-method status, null-handle rejection, and invalid-argument handling.
Documentation, AGENTS.md entries, and root wiring
AGENTS.md, crates/marmot-c/AGENTS.md, crates/marmot-c/CLAUDE.md, crates/marmot-c/README.md
Adds crate README with ABI surface, build/linking examples, and memory/threading contract; adds AGENTS.md with ABI invariants and verification commands; adds CLAUDE.md symlink; inserts navigation entry in the repo-level AGENTS.md.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially addresses #138 but has two unresolved blocking issues from adversarial review: cryptographically weak stream ID generation and incomplete agent text stream surface (missing watch half). Apply the stream ID fix (OsRng.fill_bytes) already committed and implement agent_stream.watch or explicitly defer as tracked follow-up to keep #138 open.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the primary change: introducing C bindings for the Marmot runtime via a new marmot-c crate.
Out of Scope Changes check ✅ Passed All changes are scoped to the new marmot-c crate and minimal workspace registration; no unrelated modifications to existing code outside workspace membership.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pip/darkmatter-138

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@stage-review

stage-review Bot commented Jun 14, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 7 individual chapters for you:

Title
1 Define the marmot-c crate and dependencies
2 Implement the internal C runtime handle
3 Implement JSON method dispatch logic
4 Expose the C ABI and entrypoints
5 Provide C header and integration files
6 Add documentation and agent registration
7 Other changes
Open in Stage

Chapters generated by Stage for commit d5ab658 on Jun 14, 2026 8:39pm UTC.

@agent-p1p

Copy link
Copy Markdown
Contributor Author

Adversarial review: changes requested

Blocking findings:

  1. agent_stream.start generates stream IDs with std::collections::hash_map::RandomState instead of the CSPRNG used by the existing UniFFI/QUIC surfaces.

    crates/marmot-c/src/dispatch.rs:176-189 claims "32 bytes of OS randomness" but hashes the current second plus a pointer with RandomState. That is not the same contract as marmot-uniffi (rand::rngs::OsRng) or transport-quic-stream::random_stream_id() (OsRng.fill_bytes). Stream IDs are published into the agent text stream protocol and feed the stream transcript/crypto lookup path, so the C binding should not silently weaken or fork the existing generation logic. Use the existing transport_quic_stream::random_stream_id() helper, or add rand and match the UniFFI implementation directly.

  2. The PR still does not expose the watch/subscription half of the agent text stream surface, so it should not close Add C bindings for the Marmot runtime #138 as "same surface UniFFI exposes today."

    The new C method catalogue only has agent_stream.start (crates/marmot-c/src/dispatch.rs:370-391; include/marmot.h:93-94). UniFFI exposes both start_agent_text_stream and watch_agent_text_stream (crates/marmot-uniffi/src/commands/agent_stream.rs:33-94), and the issue explicitly asked for coverage of the same runtime surface, including agent text streams. As written, C consumers can publish a stream anchor but cannot watch/read a live agent text stream through this ABI. Either add a C callback/polling handle for the watch path now, or make this PR explicitly partial and leave Add C bindings for the Marmot runtime #138 open with a tracked follow-up instead of Closes #138.

Non-blocking notes:

  • I could not run local Rust checks in this reviewer environment because cargo/rustc are not installed on PATH. Live GitHub checks were fetched; several had already passed and several were still pending at review time.

Sensitive paths touched:

  • crates/marmot-c/ is a new public C ABI over account/group/message/runtime operations and keychain-backed account construction. No MLS/CGKA internals changed, but this is new runtime attack surface.

Replace the hand-rolled RandomState/Hasher stream-id generator with
OsRng.fill_bytes, matching marmot-uniffi's random_agent_stream_id and
the workspace transport_quic_stream::random_stream_id generator.

Addresses adversarial-review blocking finding #1 on PR #410.
@agent-p1p

Copy link
Copy Markdown
Contributor Author

Address-review: both blocking findings resolved

Finding 1 — stream-ID CSPRNG (crates/marmot-c/src/dispatch.rs): Replaced the hand-rolled RandomState/Hasher generator with OsRng.fill_bytes, matching marmot-uniffi's random_agent_stream_id and the workspace transport_quic_stream::random_stream_id generator. Commit e2dea2e.

Finding 2 — same-surface scope / watch_agent_text_stream: Taken as the explicitly-sanctioned partial path. This PR is now scoped as Part of #138 (no longer Closes), so #138 stays open. The deferred live-subscription surface (UniFFI's watch_agent_text_stream + the other *Subscription objects) is filed as tracked follow-up #411 and referenced from both the PR body and crates/marmot-c/README.md.

Local checks on the new commit: cargo fmt -p marmot-c --check, RUSTFLAGS=-D warnings cargo check -p marmot-c --all-targets --locked, cargo clippy -p marmot-c --all-targets --locked -- -D warnings, cargo test -p marmot-c --locked (4/4) — all clean.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/marmot-c/src/dispatch.rs`:
- Around line 365-387: The dispatch.rs file implements the agent_stream.start
handler but is missing the corresponding agent_stream.watch handler, leaving the
C binding API incomplete for agent text streams. Add a new match arm for
"agent_stream.watch" in the dispatch function that follows the same pattern as
the existing agent_stream.start handler, parsing the appropriate request
arguments and calling the corresponding kit.runtime method to watch the agent
text stream.
- Around line 199-202: The account.list handler is directly serializing the
result from managed_accounts() without an explicit projection to the AccountInfo
DTO, which violates the coding guideline requiring explicit request/response
DTOs in dispatch.rs to prevent silent contract drift. Map the result of
managed_accounts() into an explicit Vec<AccountInfo> before serializing,
creating an intermediate variable that captures the intended JSON contract
shape, then serialize that DTO instead of the raw managed_accounts() result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34ecab54-39c8-4fb5-8bdc-d4596aefca47

📥 Commits

Reviewing files that changed from the base of the PR and between aa30672 and e2dea2e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • AGENTS.md
  • Cargo.toml
  • crates/marmot-c/AGENTS.md
  • crates/marmot-c/CLAUDE.md
  • crates/marmot-c/Cargo.toml
  • crates/marmot-c/README.md
  • crates/marmot-c/include/marmot.h
  • crates/marmot-c/marmot-c.pc.in
  • crates/marmot-c/src/dispatch.rs
  • crates/marmot-c/src/lib.rs
  • crates/marmot-c/src/runtime.rs

Comment thread crates/marmot-c/src/dispatch.rs
Comment thread crates/marmot-c/src/dispatch.rs
managed_accounts() returns Vec<ManagedAccount>; serialize through an
explicit Vec<AccountInfo> projection so the documented C JSON contract
(account.list -> [AccountInfo]) cannot silently drift from the runtime
record, per the crate's dispatch DTO invariant.

Addresses CodeRabbit review on PR #410.
@agent-p1p agent-p1p marked this pull request as ready for review June 14, 2026 20:48
@agent-p1p

Copy link
Copy Markdown
Contributor Author

Closing: issue #138 was accidentally labeled agent-ok and has since had the label removed. This PR was generated in error and is not wanted.

@agent-p1p agent-p1p closed this Jun 15, 2026
@agent-p1p agent-p1p deleted the pip/darkmatter-138 branch June 15, 2026 05:01
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