Skip to content

Add marmot-c C ABI bindings for the Marmot runtime#789

Open
dannym-arx wants to merge 6 commits into
masterfrom
c-bindings
Open

Add marmot-c C ABI bindings for the Marmot runtime#789
dannym-arx wants to merge 6 commits into
masterfrom
c-bindings

Conversation

@dannym-arx

@dannym-arx dannym-arx commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

marmot

Marmots who can't pull in a UniFFI runtime had no way to talk to the Marmot runtime. This adds marmot-c: a stable C ABI over marmot-uniffi, so C/C++ and raw-FFI callers (Zig, Nim, Go, Odin, Lua, PHP) can drive it directly.

What's in it

  • cdylib + staticlib build, with a cbindgen-generated include/marmot.h checked into the repo and diff-gated in CI.
  • Every runtime record and enum has a #[repr(C)] mirror built from the marmot-uniffi ...Ffi types, so the C shape stays in lockstep with the Swift/Kotlin surface. Values cross as owned pointers freed only by their matching marmot_*_free; inputs are borrowed and never retained.
  • Async runtime work runs on an embedded tokio runtime through blocking calls. Subscriptions offer blocking *_next reads or callback registration.
  • Memory-safety scaffolding, because the point is to hand C a stable ABI and not a footgun: an alloc-audit test feature that proves every deep-free releases what it allocated, catch_unwind at every boundary so a panic never crosses into C, and a typed MarmotStatus code per error variant with a thread-local last-error channel.

Verification

c-smoke.sh builds and runs seven worked example consumers, one per language, each driving the same slice of the runtime: lifecycle, markdown parsing, offline reads, the error taxonomy, and best-effort identity. The C, Zig, and Nim examples also walk the recursive Markdown tagged-union tree. Together they cover both linkage models (static archive and shared object) and both binding styles (header import and hand-declared decls). The C example runs under valgrind in CI.

Closes #328


Open in Stage

Summary by CodeRabbit

  • New Features
    • Added official Marmot C bindings with a versioned Linux x86_64 distribution bundle (zip, manifest, checksums) published for marmotc-v<version> tag releases.
    • Expanded the C ABI surface (client lifecycle, accounts, groups, messaging, media, notifications, relays/telemetry, timeline access, and subscriptions), plus a pkg-config file and bundled marmot.h.
  • Tests
    • Added C bindings CI smoke tests with header regeneration diff-gating and GCC/Clang runs, including Valgrind when available and multiple language consumers.
  • Documentation
    • Added/updated Marmot C bindings docs and release guidance for the new marmotc-v* track.

Open the burrow to every language: a stable C ABI over marmot-uniffi so
consumers without a UniFFI runtime (C/C++, Zig, Nim, Go, Odin, Lua, PHP)
can drive the Marmot runtime directly.

- cdylib + staticlib, cbindgen-generated checked-in include/marmot.h
- #[repr(C)] mirrors built from the marmot-uniffi ...Ffi types, so the C
  shape can never drift from the Swift/Kotlin surface
- owned pointers freed only by their matching marmot_*_free; inputs
  borrowed; embedded tokio runtime drives async work via blocking calls;
  subscriptions expose blocking next + callbacks
- memory-safety scaffolding: alloc-audit test feature, catch_unwind at
  every boundary, typed MarmotStatus + thread-local last-error
- c-bindings.sh packaging, c-smoke.sh, seven worked example consumers
  (smoke.{c,zig,nim,go,odin,lua,php}) run in CI, and a marmotc-v* release
  track

Closes #328
@stage-review

stage-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 918d3f69-7f69-4529-9582-0d9a87ab0eab

📥 Commits

Reviewing files that changed from the base of the PR and between 8d211bf and 43e035a.

📒 Files selected for processing (1)
  • crates/marmot-c/examples/smoke.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/marmot-c/examples/smoke.go

Walkthrough

This PR adds a new marmot-c crate that exposes the Marmot runtime through a stable C ABI. It wires workspace, CI, release, packaging, memory-ownership, command, type-mirror, subscription, and multi-language smoke-test support around that surface.

Changes

Marmot C ABI Bindings

Layer / File(s) Summary
Workflow and workspace wiring
.github/workflows/c-bindings.yml, .github/workflows/ci.yml, AGENTS.md, Cargo.toml, Justfile, release.md
Adds the tag-triggered release workflow, CI smoke-test job, workspace member/dependency wiring, Just targets, and release documentation for the C bindings.
Crate scaffold and packaging files
crates/marmot-c/Cargo.toml, crates/marmot-c/.gitignore, crates/marmot-c/AGENTS.md, crates/marmot-c/CHANGELOG.md, crates/marmot-c/CLAUDE.md, crates/marmot-c/README.md, crates/marmot-c/cbindgen.toml, crates/marmot-c/marmot-c.pc.in, crates/marmot-c/c-bindings.sh, crates/marmot-uniffi/src/lib.rs
Adds the crate manifest, docs, cbindgen and pkg-config configuration, build script, generated-artifact ignore rules, and public UniFFI module visibility needed by the C surface.
ABI foundation: client, memory, and status
crates/marmot-c/src/lib.rs, crates/marmot-c/src/memory.rs, crates/marmot-c/src/status.rs
Implements the opaque client handle, panic containment, out-pointer helpers, allocation and borrowing helpers, and stable status-code mapping with thread-local last-error storage.
Command wrappers
crates/marmot-c/src/commands/*.rs
Adds shared delivery helpers and exported FFI wrappers for account, agent-stream, audit, chat-list, directory, group, media, message, notification, push, relay, and timeline operations.
Mirror types and free functions
crates/marmot-c/src/types/*.rs
Defines repr(C) mirror structs and enums with deep conversions and CFree implementations, plus exported free functions, for account, agent-stream, audit, chat-list, common, event, group, markdown, media, message, notification, push, relay, and timeline data.
Subscription APIs
crates/marmot-c/src/subscriptions.rs
Implements subscription core behavior, blocking next/timeout semantics, callback pumps, and exported subscribe/snapshot/next/callback/free APIs for events, timeline messages, notifications, chats, chat-list, messages, group state, and agent streams.
Smoke examples and runner
crates/marmot-c/c-smoke.sh, crates/marmot-c/examples/*
Adds the multi-toolchain smoke runner and end-to-end C, Go, Lua, Nim, Odin, PHP, and Zig consumers that exercise client lifecycle, markdown parsing, account reads, error taxonomy, and identity creation.

Estimated code review effort: 5 (Critical) | ~150 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 is concise and accurately summarizes the main addition: a new C ABI bindings crate for the Marmot runtime.
Linked Issues check ✅ Passed The PR implements a stable C ABI crate with generated headers, cdylib/staticlib builds, broad runtime coverage, and packaging guidance.
Out of Scope Changes check ✅ Passed The extra workflows, docs, and example consumers support the C ABI release and verification flow rather than introducing unrelated scope.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch c-bindings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

@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: 10

🧹 Nitpick comments (2)
.github/workflows/ci.yml (1)

129-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin the cbindgen version used for the header diff gate.

cargo install cbindgen --locked pulls whatever is currently published on crates.io. cbindgen's output formatting can change across releases, so this gate can start failing (or silently drift) on unrelated PRs whenever a new cbindgen version ships, rather than only when the ABI actually changes.

♻️ Proposed fix: pin an explicit version
-          cargo install cbindgen --locked
+          cargo install cbindgen --version <pinned-version> --locked

Please confirm the intended pinned cbindgen version (and that crates/marmot-c/cbindgen.toml's syntax is compatible with it), since this repo's Cargo manifests weren't in the current review context.

🤖 Prompt for 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.

In @.github/workflows/ci.yml around lines 129 - 135, The header diff gate
currently installs cbindgen without a version, so its output can drift when
crates.io releases change. Update the CI job in the workflow’s “Header diff
gate” step to install a specific cbindgen version instead of using cargo install
cbindgen --locked, and make sure the pinned version is compatible with
crates/marmot-c/cbindgen.toml. Use the existing cbindgen invocation and git diff
check unchanged after pinning the version.
crates/marmot-c/examples/smoke.php (1)

121-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

PHPStan can't resolve the @var \FFI&MarmotFfi intersection type.

A real \FFI instance never actually implements the hand-authored MarmotFfi interface (it only exists for @method documentation), so PHPStan correctly reports this intersection as unresolvable at every use site. It's harmless at runtime but means these annotations don't buy real type-checking and will keep surfacing as PHPStan errors.

Also applies to: 309-322

🤖 Prompt for 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.

In `@crates/marmot-c/examples/smoke.php` around lines 121 - 141, The `@var`
annotations using the \FFI&MarmotFfi intersection in smoke.php are not
resolvable by PHPStan because the runtime FFI instance does not actually
implement MarmotFfi. Update the type annotations around the FFI setup in the
client constructor/open flow to use a PHPStan-resolvable type that still
documents the available methods, and remove the invalid intersection at each
affected use site including the later block around the other reported lines.

Source: Linters/SAST tools

🤖 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 @.github/workflows/c-bindings.yml:
- Around line 47-48: The release workflow’s rust cache step currently enables
cache restore/write behavior by default, which can reuse untrusted artifacts.
Update the Cache Cargo artifacts step in the .github workflow to disable cache
writes and avoid cross-branch reuse on the tag-triggered release path, using the
Swatinem/rust-cache configuration options on that step so the published
libmarmot_c.so/.a artifacts are built without trusting prior cache contents.

In `@crates/marmot-c/examples/smoke.nim`:
- Around line 178-185: The relay argument arrays are built from temporary Nim
string views via cstring, so they may not stay valid for the FFI calls. Update
the open helper and the matching identity-creation path to use owned C string
storage that survives until after marmot_client_new and marmot_create_identity
return, such as allocCStringArray with a corresponding deallocation afterward.
Keep the fix localized around the existing open proc and the create-identity
code that builds relay arrays.

In `@crates/marmot-c/examples/smoke.php`:
- Around line 289-307: marshalStrings() is allocating relay buffers with
owned:false, which leaks memory because nothing ever frees them. Update the
buffer allocation in marshalStrings() to use PHP-owned CData so the $buffers
pinning works as intended, and keep the existing array of backing buffers tied
to the FFI call via marshalStrings()/FFI::addr() usage. Ensure the fix preserves
the current char*[] marshaling behavior while removing the manual-lifetime
mismatch.

In `@crates/marmot-c/src/subscriptions.rs`:
- Around line 251-258: The subscription handle created in this path can leak
when write_out returns Err because ownership never transfers, so explicitly free
the boxed handle on the error branch before returning the status. Update the
same error-path cleanup pattern in the other marmot_subscribe_* constructors and
in marmot_watch_agent_text_stream, using the local handle/boxed(handle) flow in
each function to ensure failed write_out calls do not leak memory.
- Around line 121-135: The callback registration flow in install and
spawn_callback_pump allows a new pump to start before the existing callback
check, so a rejected registration can still invoke callback(user_data) or
consume items before task.abort() takes effect. Update the callback setup so the
callback slot is reserved under the mutex before any task is spawned, for
example by moving pump creation behind install or letting install accept a
closure and spawn only after the slot is successfully claimed. Keep the logic
anchored around install, callback_task, and spawn_callback_pump so failed
registrations cannot start work.

In `@crates/marmot-c/src/types/event.rs`:
- Around line 424-427: Wrap marmot_event_free in ffi_guard so the exported C
free path cannot unwind across the FFI boundary. Update marmot_event_free to
invoke free_boxed through ffi_guard, and apply the same pattern to the other
marmot_*_free functions under src/types/ so all exported frees use the shared
panic guard.

In `@crates/marmot-c/src/types/group.rs`:
- Around line 822-826: The roundtrip tests in group_record_deep_roundtrip and
the other similar tests in group.rs are gating memory::audit::test_lock() behind
#[cfg(feature = "alloc-audit")], which contradicts the documented locking
contract. Update those tests to take the test_lock guard unconditionally,
matching the pattern used in common.rs and relay.rs, while keeping the
alloc-audit-only allocation checks feature-gated as they are.

In `@crates/marmot-c/src/types/media.rs`:
- Around line 372-375: The exported free functions in the media FFI layer are
missing the same panic fence used by other C entry points. Update
marmot_media_upload_result_free, marmot_media_download_result_free, and
marmot_media_record_list_free in media.rs to route their bodies through
ffi_guard, matching the extern "C" wrappers used elsewhere in lib.rs, while
keeping the existing free_boxed cleanup behavior inside the guard.

In `@crates/marmot-c/src/types/notification.rs`:
- Around line 141-146: Wrap the exported free function
marmot_notification_settings_free in ffi_guard, matching the marmot_client_free
pattern, so any panic from free_boxed is contained before crossing the C ABI.
Update the body to invoke free_boxed through ffi_guard and keep the unsafe
extern "C" signature unchanged, using the existing FFI guard helper in this
module or nearby FFI code.

In `@crates/marmot-c/src/types/push.rs`:
- Around line 106-109: Wrap the exported C free functions in ffi_guard so panics
cannot unwind across the FFI boundary. Update marmot_push_registration_free and
marmot_group_push_debug_info_free in the push.rs module to call free_boxed from
inside ffi_guard, matching the pattern used by the other exported free helpers
in this crate. Keep the existing unsafe extern "C" signatures and only change
the function bodies to route the deep-free work through ffi_guard.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 129-135: The header diff gate currently installs cbindgen without
a version, so its output can drift when crates.io releases change. Update the CI
job in the workflow’s “Header diff gate” step to install a specific cbindgen
version instead of using cargo install cbindgen --locked, and make sure the
pinned version is compatible with crates/marmot-c/cbindgen.toml. Use the
existing cbindgen invocation and git diff check unchanged after pinning the
version.

In `@crates/marmot-c/examples/smoke.php`:
- Around line 121-141: The `@var` annotations using the \FFI&MarmotFfi
intersection in smoke.php are not resolvable by PHPStan because the runtime FFI
instance does not actually implement MarmotFfi. Update the type annotations
around the FFI setup in the client constructor/open flow to use a
PHPStan-resolvable type that still documents the available methods, and remove
the invalid intersection at each affected use site including the later block
around the other reported lines.
🪄 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: fe634a61-cf9a-4fa6-9b99-0fd35b462424

📥 Commits

Reviewing files that changed from the base of the PR and between 400d223 and 462ed2e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (58)
  • .github/workflows/c-bindings.yml
  • .github/workflows/ci.yml
  • AGENTS.md
  • Cargo.toml
  • Justfile
  • crates/marmot-c/.gitignore
  • crates/marmot-c/AGENTS.md
  • crates/marmot-c/CHANGELOG.md
  • crates/marmot-c/CLAUDE.md
  • crates/marmot-c/Cargo.toml
  • crates/marmot-c/README.md
  • crates/marmot-c/c-bindings.sh
  • crates/marmot-c/c-smoke.sh
  • crates/marmot-c/cbindgen.toml
  • crates/marmot-c/examples/smoke.c
  • crates/marmot-c/examples/smoke.go
  • crates/marmot-c/examples/smoke.lua
  • crates/marmot-c/examples/smoke.nim
  • crates/marmot-c/examples/smoke.odin
  • crates/marmot-c/examples/smoke.php
  • crates/marmot-c/examples/smoke.zig
  • crates/marmot-c/include/marmot.h
  • crates/marmot-c/marmot-c.pc.in
  • crates/marmot-c/src/commands/account.rs
  • crates/marmot-c/src/commands/agent_stream.rs
  • crates/marmot-c/src/commands/audit.rs
  • crates/marmot-c/src/commands/chat_list.rs
  • crates/marmot-c/src/commands/directory.rs
  • crates/marmot-c/src/commands/group.rs
  • crates/marmot-c/src/commands/media.rs
  • crates/marmot-c/src/commands/message.rs
  • crates/marmot-c/src/commands/mod.rs
  • crates/marmot-c/src/commands/notification.rs
  • crates/marmot-c/src/commands/push.rs
  • crates/marmot-c/src/commands/relay.rs
  • crates/marmot-c/src/commands/subscription.rs
  • crates/marmot-c/src/commands/timeline.rs
  • crates/marmot-c/src/lib.rs
  • crates/marmot-c/src/memory.rs
  • crates/marmot-c/src/status.rs
  • crates/marmot-c/src/subscriptions.rs
  • crates/marmot-c/src/types/account.rs
  • crates/marmot-c/src/types/agent_stream.rs
  • crates/marmot-c/src/types/audit.rs
  • crates/marmot-c/src/types/chat_list.rs
  • crates/marmot-c/src/types/common.rs
  • crates/marmot-c/src/types/event.rs
  • crates/marmot-c/src/types/group.rs
  • crates/marmot-c/src/types/markdown.rs
  • crates/marmot-c/src/types/media.rs
  • crates/marmot-c/src/types/message.rs
  • crates/marmot-c/src/types/mod.rs
  • crates/marmot-c/src/types/notification.rs
  • crates/marmot-c/src/types/push.rs
  • crates/marmot-c/src/types/relay.rs
  • crates/marmot-c/src/types/timeline.rs
  • crates/marmot-uniffi/src/lib.rs
  • release.md

Comment thread .github/workflows/c-bindings.yml
Comment thread crates/marmot-c/examples/smoke.nim
Comment thread crates/marmot-c/examples/smoke.php
Comment thread crates/marmot-c/src/subscriptions.rs Outdated
Comment thread crates/marmot-c/src/subscriptions.rs
Comment thread crates/marmot-c/src/types/event.rs
Comment thread crates/marmot-c/src/types/group.rs
Comment thread crates/marmot-c/src/types/media.rs
Comment thread crates/marmot-c/src/types/notification.rs
Comment thread crates/marmot-c/src/types/push.rs
Keep the burrow airtight after review:

- fmt: collapse the status.rs ExternalSigner match arm
- CI: set save-if: false on the tag-triggered release cache so a poisoned
  PR cache can't be baked into published libmarmot_c artifacts
- subscriptions: reserve the callback slot before spawning the pump (no
  pump races the receiver then gets aborted), and free the boxed handle
  when the out-pointer write fails
- panic-safety: wrap all 55 exported marmot_*_free bodies in free_guard
  (catch_unwind) so a deep-free panic never unwinds into C
- tests: take the alloc-audit lock unconditionally to match its contract
- examples/nim: use allocCStringArray so relay arrays outlive the FFI call
  instead of dangling loop-local cstrings under ARC/ORC
- examples/php: own the marshalled relay buffers so PHP frees them

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/marmot-c/examples/smoke.php (1)

293-296: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Silence the PHPStan by-ref type mismatch on $buffers.

PHPStan flags line 305: $ffi->new() is typed FFI\CData|null, so appending to $buffers widens it beyond the declared list<\FFI\CData> in-type. Add a @param-out tag to declare the out-type explicitly.

🩹 Proposed PHPDoc fix
      * `@param` list<string>       $strings
-     * `@param` list<\FFI\CData>   $buffers out-param: backing buffers to pin
+     * `@param` list<\FFI\CData>   $buffers out-param: backing buffers to pin
+     * `@param-out` list<\FFI\CData> $buffers
      */
🤖 Prompt for 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.

In `@crates/marmot-c/examples/smoke.php` around lines 293 - 296, The PHPStan
warning is caused by the by-ref $buffers parameter in marshalStrings being
treated as only list<\FFI\CData> while the method appends results from
$ffi->new(), which can be FFI\CData|null. Update the PHPDoc on marshalStrings to
declare the out-type for $buffers explicitly with a `@param-out` tag so PHPStan
understands the post-call shape, keeping the existing by-ref behavior and the
current method signature intact.

Source: Linters/SAST tools

🤖 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.

Outside diff comments:
In `@crates/marmot-c/examples/smoke.php`:
- Around line 293-296: The PHPStan warning is caused by the by-ref $buffers
parameter in marshalStrings being treated as only list<\FFI\CData> while the
method appends results from $ffi->new(), which can be FFI\CData|null. Update the
PHPDoc on marshalStrings to declare the out-type for $buffers explicitly with a
`@param-out` tag so PHPStan understands the post-call shape, keeping the existing
by-ref behavior and the current method signature intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca52b6db-47dc-49bb-b7cb-0ff203704ba4

📥 Commits

Reviewing files that changed from the base of the PR and between 462ed2e and 0ad4a5e.

📒 Files selected for processing (20)
  • .github/workflows/c-bindings.yml
  • crates/marmot-c/examples/smoke.nim
  • crates/marmot-c/examples/smoke.php
  • crates/marmot-c/src/memory.rs
  • crates/marmot-c/src/status.rs
  • crates/marmot-c/src/subscriptions.rs
  • crates/marmot-c/src/types/account.rs
  • crates/marmot-c/src/types/agent_stream.rs
  • crates/marmot-c/src/types/audit.rs
  • crates/marmot-c/src/types/chat_list.rs
  • crates/marmot-c/src/types/common.rs
  • crates/marmot-c/src/types/event.rs
  • crates/marmot-c/src/types/group.rs
  • crates/marmot-c/src/types/markdown.rs
  • crates/marmot-c/src/types/media.rs
  • crates/marmot-c/src/types/message.rs
  • crates/marmot-c/src/types/notification.rs
  • crates/marmot-c/src/types/push.rs
  • crates/marmot-c/src/types/relay.rs
  • crates/marmot-c/src/types/timeline.rs
🚧 Files skipped from review as they are similar to previous changes (19)
  • crates/marmot-c/src/types/common.rs
  • crates/marmot-c/src/status.rs
  • crates/marmot-c/src/types/notification.rs
  • .github/workflows/c-bindings.yml
  • crates/marmot-c/src/types/account.rs
  • crates/marmot-c/src/types/timeline.rs
  • crates/marmot-c/src/memory.rs
  • crates/marmot-c/src/types/agent_stream.rs
  • crates/marmot-c/src/types/event.rs
  • crates/marmot-c/src/types/relay.rs
  • crates/marmot-c/src/types/chat_list.rs
  • crates/marmot-c/src/types/media.rs
  • crates/marmot-c/src/types/markdown.rs
  • crates/marmot-c/src/types/push.rs
  • crates/marmot-c/src/types/audit.rs
  • crates/marmot-c/src/types/message.rs
  • crates/marmot-c/examples/smoke.nim
  • crates/marmot-c/src/types/group.rs
  • crates/marmot-c/src/subscriptions.rs

Fill a parity gap from the upstream rebase. The group record already
carries image_hash_hex; this adds the fetch/verify/decrypt call that
turns it into pixels.

- marmot_download_group_blossom_image → owned byte buffer + out_len
- marmot_bytes_free for byte-buffer returns (panic-guarded, NULL-safe)
- deliver_bytes helper with unit tests (pair write, empty->NULL, NULL-out
  rejection, error mapping)
- C example exercises the command + NULL-safe free
- document external-signer accounts as a deliberate deferred gap (needs a
  callback vtable) in AGENTS.md
Lock in the review fixes so they can't silently regress:

- SubscriptionCore: block_next returns data / closed / times out; install
  reserves the slot before spawning and rejects a second callback without
  running its spawn closure; accepts again after clear
- free_guard swallows a panic from a free/Drop path (never unwinds into C)
Address pre-merge review:

- Callback user_data lifetime docs were unsafe. clear_callback and
  *_subscription_free call JoinHandle::abort (non-blocking), so a callback
  can still be running after they return. The old docs implied user_data
  could be freed right after clear/free. Rewrote the module contract, the
  set_callback Safety notes, the clear_callback docs, and all eight free-fn
  docs to state the real rule: cancellation is a request, not a barrier;
  user_data must outlive every callback, and the caller must use its own
  synchronization to know none is in flight. Making abort blocking would
  risk deadlock (the pump may be inside the callback), so the wait stays
  with the caller.
- MarmotStatus: give the discriminant a fixed cross-platform width. Use
  #[repr(i32)] (a bare C enum's width is implementation-defined). Note:
  #[repr(C, i32)] is only valid on enums with fields; on this fieldless
  enum it is a compile error, and repr(i32) yields the same int32_t-backed
  header emission. Regenerated marmot.h.
- CI: pin cbindgen to 0.29.4 in the header diff gate so a newer release
  can't reformat the checked-in header and fail unrelated PRs.
The #[repr(i32)] switch made cgo map MarmotStatus to int32 instead of
uint32, so smoke.go's uint32-typed check() no longer compiled on a clean
build (a local Go build cache had masked it). Type check() and the status
comparisons as C.MarmotStatus so the example is agnostic to whether cgo
sees the enum as signed or unsigned.
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.

Add C bindings for the Marmot runtime

1 participant