-
Notifications
You must be signed in to change notification settings - Fork 30
Add marmot-c C ABI bindings for the Marmot runtime #789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dannym-arx
wants to merge
6
commits into
master
Choose a base branch
from
c-bindings
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
462ed2e
Add marmot-c C ABI bindings for the Marmot runtime
dannym-arx 0ad4a5e
marmot-c: address CodeRabbit review and fix formatting
dannym-arx 536a21a
marmot-c: add group Blossom image download command
dannym-arx f86ed49
marmot-c: test the subscription slot logic and panic guard
dannym-arx 8d211bf
marmot-c: correct callback lifetime docs; fix status width; pin cbindgen
dannym-arx 43e035a
marmot-c: fix Go example after MarmotStatus repr change
dannym-arx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| name: Marmot C Bindings | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - marmotc-v* | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: marmot-c-bindings-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| CARGO_TERM_COLOR: always | ||
| CARGO_INCREMENTAL: "0" | ||
| CARGO_PROFILE_RELEASE_DEBUG: "0" | ||
|
|
||
| jobs: | ||
| linux-bindings: | ||
| name: Build Linux C bindings | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
|
|
||
| steps: | ||
| - name: Check out repository | ||
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Validate release tag | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ ! "$GITHUB_REF_NAME" =~ ^marmotc-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then | ||
| echo "error: C binding releases must use a version tag like marmotc-v0.1.0" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Install Rust toolchain | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| rustup show active-toolchain || rustup toolchain install | ||
|
|
||
| - name: Cache Cargo artifacts | ||
| uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 | ||
| with: | ||
| # Release artifacts are published to a public GitHub Release, so | ||
| # never write (or reuse a poisoned) cache on this tag-triggered path. | ||
| save-if: false | ||
|
|
||
| - name: Build C bindings | ||
| shell: bash | ||
| run: ./crates/marmot-c/c-bindings.sh | ||
|
|
||
| - name: Smoke-test against gcc and clang | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| sudo apt-get update | ||
| sudo apt-get install -y valgrind | ||
| ./crates/marmot-c/c-smoke.sh gcc clang | ||
|
|
||
| - name: Package bundle | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| tag="$GITHUB_REF_NAME" | ||
| version="${tag#marmotc-v}" | ||
| workspace_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)" | ||
| lock_sha="$(sha256sum Cargo.lock | awk '{print $1}')" | ||
|
|
||
| stage_parent="build/marmot-c-stage" | ||
| stage="$stage_parent/marmot-c-$version" | ||
| asset="marmot-c-linux-x86_64-$version.zip" | ||
| rm -rf "$stage_parent" "$asset" "$asset.sha256" | ||
| mkdir -p "$stage" dist | ||
|
|
||
| cp -R crates/marmot-c/output/. "$stage/" | ||
| cp crates/marmot-c/README.md "$stage/" | ||
| cat > "$stage/manifest.json" <<EOF | ||
| { | ||
| "artifact": "marmot-c", | ||
| "version": "$version", | ||
| "workspace_version": "$workspace_version", | ||
| "cargo_lock_sha256": "$lock_sha", | ||
| "commit": "$GITHUB_SHA", | ||
| "target": "x86_64-unknown-linux-gnu" | ||
| } | ||
| EOF | ||
|
|
||
| (cd "$stage_parent" && zip -r "../../$asset" "marmot-c-$version") | ||
| sha256sum "$asset" | awk -v name="$asset" '{print $1 " " name}' > "$asset.sha256" | ||
| cp "$asset" "$asset.sha256" dist/ | ||
|
|
||
| - name: Upload bundle | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: marmot-c-linux | ||
| path: dist/ | ||
| if-no-files-found: error | ||
|
|
||
| release: | ||
| name: Create GitHub release | ||
| runs-on: ubuntu-latest | ||
| needs: | ||
| - linux-bindings | ||
| timeout-minutes: 15 | ||
| permissions: | ||
| actions: read | ||
| contents: write | ||
|
|
||
| steps: | ||
| - name: Download binding artifacts | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| set -euo pipefail | ||
| mkdir -p dist | ||
| gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" --name marmot-c-linux --dir dist | ||
|
|
||
| - name: Create or update release | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| set -euo pipefail | ||
| tag="$GITHUB_REF_NAME" | ||
| version="${tag#marmotc-v}" | ||
| notes="$RUNNER_TEMP/release-notes.md" | ||
| cat > "$notes" <<EOF | ||
| Marmot C bindings generated from MDK commit \`$GITHUB_SHA\`. | ||
|
|
||
| Assets: | ||
| - \`marmot-c-linux-x86_64-$version.zip\`: \`libmarmot_c.so\` + \`libmarmot_c.a\`, \`marmot.h\`, pkg-config file, README. | ||
|
|
||
| Verify downloads with the accompanying \`.sha256\` files. | ||
| EOF | ||
|
|
||
| assets=( | ||
| "dist/marmot-c-linux-x86_64-$version.zip" | ||
| "dist/marmot-c-linux-x86_64-$version.zip.sha256" | ||
| ) | ||
|
|
||
| if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | ||
| gh release upload "$tag" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber | ||
| gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --title "v$version - Marmot C" --notes-file "$notes" | ||
| else | ||
| gh release create "$tag" "${assets[@]}" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --title "v$version - Marmot C" \ | ||
| --notes-file "$notes" | ||
| fi | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Generated/derived build artifacts. c-bindings.sh writes into output/ and | ||
| # c-smoke.sh writes into build/. The checked-in include/marmot.h header is the | ||
| # source of truth for the C surface (CI diff-gates it); nothing else here is. | ||
| /build/ | ||
| /output/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # AGENTS.md - marmot-c | ||
|
|
||
| C ABI bindings for the Marmot app runtime. Read `README.md` first for build, linking, and packaging. | ||
|
|
||
| ## Scope | ||
|
|
||
| - Own the stable C ABI over `marmot-uniffi` for C/C++, Zig, Odin, and raw-FFI consumers (Go, Lua, …). | ||
| - Own `include/marmot.h` (generated by cbindgen, checked in) and `cbindgen.toml`. | ||
| - Own `c-bindings.sh` (cdylib + staticlib + header + pkg-config staging into `output/`) and the `examples/` consumers | ||
| (`smoke.{c,zig,nim,go,odin,lua,php}`) driven by `c-smoke.sh`. | ||
|
|
||
| ## Known gaps | ||
|
|
||
| - External-signer accounts (`register_external_signer` / `login_external_signer`) are not yet exposed. They take a | ||
| host-implemented `ExternalAccountSignerFfi` trait (public key, sign event, NIP-04 encrypt/decrypt). A C mapping needs a | ||
| callback vtable (function pointers + `user_data`) invoked from runtime worker threads, with owned-string return and | ||
| error signalling across the boundary. That deserves its own focused design + tests rather than a rushed vtable. | ||
|
|
||
| ## Architecture | ||
|
|
||
| - `marmot-uniffi` is the single source of truth for FFI DTO shapes and the runtime object; this crate never talks to | ||
| `marmot-app` directly. Every `…Ffi` record/enum has a `#[repr(C)]` mirror in `src/types/` (one module per | ||
| `marmot-uniffi` conversions module) built by `From<…Ffi>` and released by a `CFree` deep-free. | ||
| - `src/commands/` holds the `extern "C"` wrappers (one module per `marmot-uniffi` commands module): `ffi_guard` | ||
| (catch_unwind), borrowed-input readers, `block_on` onto the embedded tokio runtime, mirror conversion, out-pointer. | ||
| - `src/subscriptions.rs` wraps the 8 subscription objects as opaque handles with blocking `*_next` (timeout → | ||
| `MARMOT_STATUS_TIMEOUT`, shutdown → `MARMOT_STATUS_CLOSED`) and optional callback registration. | ||
| - `src/memory.rs` is the only allocation chokepoint; the `alloc-audit` test feature counts live allocations so tests | ||
| can prove deep-free completeness. | ||
|
|
||
| ## Invariants | ||
|
|
||
| - No struct crossing the ABI is freed anywhere except its own `marmot_*_free`; frees are deep and NULL-tolerant; | ||
| input structs and strings are borrowed and never freed or retained. | ||
| - Every `extern "C"` body is wrapped in `ffi_guard` — panics must not unwind into C. | ||
| - `MarmotStatus` values are stable ABI: append, never renumber. Keep the variant set in lockstep with | ||
| `marmot_uniffi::MarmotKitError`. | ||
| - `include/marmot.h` is generated, never hand-edited; regenerate with `just c-header`. CI diff-gates it. | ||
| - Host-supplied `group_id_hex` values are variable-length MLS `GroupId` bytes — do not validate them with the | ||
| 32-byte route-id/pubkey/message-id rule. | ||
| - No `tracing`/logging in this crate; error detail strings carry only what `MarmotKitError` Display already exposes. | ||
| - Keep this crate in lockstep with `marmot-uniffi` surface changes; bump the workspace version when mirrors, | ||
| commands, status codes, or callback signatures change. | ||
|
|
||
| ## Verification | ||
|
|
||
| ```sh | ||
| cargo test -p marmot-c --features alloc-audit | ||
| just c-header && git diff --exit-code crates/marmot-c/include/marmot.h | ||
| just c-smoke # compiles and runs examples/smoke.c (valgrind when available) | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to `marmot-c` are tracked here. | ||
|
|
||
| The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This crate uses semantic | ||
| versioning through the workspace version in the root `Cargo.toml`. | ||
|
|
||
| ## [0.9.3] - 2026-07-08 | ||
|
|
||
| ### Added | ||
|
|
||
| - Added `marmot-c`, a stable C ABI over the Marmot app runtime for consumers that cannot pull in a UniFFI runtime | ||
| (C/C++, and raw-FFI callers such as Zig, Nim, Go, Odin, Lua, and PHP). The crate builds a `cdylib` and a `staticlib`, | ||
| ships a cbindgen-generated, checked-in `include/marmot.h`, and mirrors the full `marmot-uniffi` surface: account and | ||
| session lifecycle, group operations, messaging, media, timeline, notifications, push, audit, relay health, agent text | ||
| streams, and Markdown parsing. | ||
| - Every runtime record/enum has a `#[repr(C)]` mirror built from the `marmot-uniffi` `…Ffi` types, so the C shape can | ||
| never drift from the Swift/Kotlin surface. Complex values cross the ABI 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 via | ||
| blocking calls, and subscriptions expose both blocking `*_next` reads and callback registration. | ||
| - Added memory-safety scaffolding: an `alloc-audit` test feature that proves deep-free completeness, `catch_unwind` at | ||
| every ABI boundary so panics never unwind into C, and a typed `MarmotStatus` code per `MarmotKitError` variant with a | ||
| thread-local last-error detail channel. | ||
| - Added `marmot_download_group_blossom_image` (fetch + verify + decrypt the group's encrypted Blossom image to an owned | ||
| byte buffer freed with `marmot_bytes_free`), completing the group-image surface alongside `image_hash_hex`. External | ||
| signer accounts remain a documented gap pending a C callback-vtable design. | ||
| - Added packaging and cross-language verification: `c-bindings.sh` (staged libraries + header + pkg-config), | ||
| `c-smoke.sh`, and seven worked example consumers (`examples/smoke.{c,zig,nim,go,odin,lua,php}`) exercised in CI, plus a | ||
| `marmotc-v*` release track titled `v<version> - Marmot C`. | ||
|
|
||
| Closes [#328](https://github.com/marmot-protocol/mdk/issues/328) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| AGENTS.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| [package] | ||
| name = "marmot-c" | ||
| edition.workspace = true | ||
| version.workspace = true | ||
| license.workspace = true | ||
| publish.workspace = true | ||
|
|
||
| [lib] | ||
| name = "marmot_c" | ||
| crate-type = ["cdylib", "staticlib", "lib"] | ||
|
|
||
| [features] | ||
| default = [] | ||
| # Test-only accounting of every FFI allocation/free pair; used by the | ||
| # roundtrip tests to prove deep-free completeness. Never enable in release | ||
| # artifacts. | ||
| alloc-audit = [] | ||
|
|
||
| [dependencies] | ||
| marmot-uniffi.workspace = true | ||
| tokio = { workspace = true, features = ["io-util", "net", "time"] } | ||
| libc.workspace = true | ||
|
|
||
| [dev-dependencies] | ||
| tempfile.workspace = true |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.