feat(marmot-c): add C bindings for the Marmot runtime#410
Conversation
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
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughAdds a new Changesmarmot-c C bindings crate
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Ready to review this PR? Stage has broken it down into 7 individual chapters for you: Chapters generated by Stage for commit d5ab658 on Jun 14, 2026 8:39pm UTC. |
Adversarial review: changes requestedBlocking findings:
Non-blocking notes:
Sensitive paths touched:
|
Address-review: both blocking findings resolvedFinding 1 — stream-ID CSPRNG ( Finding 2 — same-surface scope / Local checks on the new commit: |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
AGENTS.mdCargo.tomlcrates/marmot-c/AGENTS.mdcrates/marmot-c/CLAUDE.mdcrates/marmot-c/Cargo.tomlcrates/marmot-c/README.mdcrates/marmot-c/include/marmot.hcrates/marmot-c/marmot-c.pc.incrates/marmot-c/src/dispatch.rscrates/marmot-c/src/lib.rscrates/marmot-c/src/runtime.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.
|
Closing: issue #138 was accidentally labeled |
Summary
Adds a
marmot-ccrate 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 sameMarmotApp+MarmotAppRuntimepair that the Swift/Kotlin bindings use. This is the C-surface counterpart tomarmot-uniffi, bringing the org's existingmdk-cshape 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 opaqueMarmotC *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 returnedchar *.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 viablock_onon the handle's owned tokio runtime. Most runtime records already deriveserde::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
*Subscriptionnext()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+staticlibbuild targets.include/marmot.h(cross-checked againstcbindgen --lang c src/lib.rs; signatures + constants match).pkg-configtemplate (marmot-c.pc.in) and CMake/linking notes in the README.AGENTS.md+CLAUDE.mdsymlink per repo convention; registered in the rootAGENTS.mdmap.Test Plan
cargo fmt --all --check— cleanRUSTFLAGS='-D warnings' cargo check --workspace --all-targets --locked(and--all-features) — cleancargo clippy --workspace --all-targets --locked -- -D warnings(and--all-features) — cleancargo 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, mirroringmarmot-uniffi's smoke test.Notes
Cargo.toml,Cargo.lock) and the rootAGENTS.mdmap entry.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_streamand the other*Subscriptionobjects) is deferred to a tracked follow-up, #411, so #138stays open until that lands.
Summary by CodeRabbit
Release Notes
New Features
Documentation