Ring is a cross-language IPC bus by Impulse: data channels and async function calls between processes on one machine over pure POSIX shared memory — no HTTP, no sockets, no kernel round-trips on the data path. Every message and every function argument/result is Apache Avro (binary, versioned, schema-bearing), and interface compatibility is enforced by CRC-64-AVRO schema fingerprints.
It exists to replace two things on a single host:
- Cross-language FFI that otherwise collapses to C bindings.
- Local REST / gRPC / JSON-RPC between microservices, which needlessly copy data user-space → kernel → user-space. Ring keeps payloads in shared memory.
Tier 0 target: Linux only, arm64 + amd64.
This milestone delivers the broker, the shared core, and the Rust connector, with a full end-to-end path validated by an integration test:
- ✅
impulsedbroker: owns the control segment + data arenas, registers apps, routes the control plane, garbage-collects shared memory on crash/shutdown. - ✅
impulse-ring-core: shared-memory segments, ring buffers (futex wakeup, adaptive spin), Avro framing + fingerprinting, control protocol records. - ✅
impulse-ring-connector(Rust): register, publish/subscribe (key-gated, fingerprint-checked), expose functions, async RPCcall. - ✅ Byte-for-byte wire spec in
spec/— the contract for native connectors in other languages.
Connectors are implemented natively against spec/ (no binding to the Rust
core), except where a language can't touch shared memory directly. Per project
policy, schema fingerprints are computed by the broker, so connectors send
schema JSON and use the returned fingerprints.
- ✅ C —
connectors/c/: pure C11 + POSIX. Self-test + C↔Rust cross-language test. - ✅ C++ —
connectors/cpp/: header-only RAII/STL wrapper over the C core (not a reimplementation). Self-test. - ✅ Python —
connectors/python/: pure Python protocol- a tiny C atomics/futex extension. Self-test + Python↔Rust test.
- ✅ Go —
connectors/go/: pure Go, no cgo (sync/atomic+ rawfutex). Self-test + Go↔Rust test. - ✅ JS/TS —
connectors/ts/: TypeScript over Bun FFI to the C library (JS can't mmap/atomics in pure JS). Client surface (publish/subscribe/call); TS↔Rust test.
Every connector sizes the rings it owns and reads back the sizes it does not:
publish_channel takes an arena capacity (the publisher knows how big its
messages are), connect takes a reply-ring capacity (only the caller knows how
big an answer it will accept), and the capacity of a channel arena, a reply ring
and a function's request arena is readable from either end. See each connector's
README under Ring sizes.
A connector of 0.9.0 or later needs a broker of 0.9.0 or later: PublishChannel
grew the arena_cap field, so its fingerprint changed. The reverse direction
keeps working — a 0.9.0+ broker still accepts the pre-0.9.0 record from an
unupgraded connector (see the legacy table in
spec/schemas/FINGERPRINTS.md).
New to writing schemas? See spec/authoring-schemas.md for how to describe messages and functions in Avro.
CI (.depl/config.yaml) formats and lints every language — Rust (cargo fmt
2-space/120 + clippy), C/C++ (clang-format 2-space/120 + clang-tidy),
Python (ruff via uv), Go (gofmt + go vet) — and runs each connector's
example against a live broker.
crates/
core/ shared mechanics (shm, ring, futex, frame, avro, proto, control)
impulsed/ the broker daemon (+ E2E test in tests/)
connector/ the Rust connector (+ runnable demo in examples/)
spec/
wire-format.md normative byte layout (segments, rings, frames, Avro)
bootstrap.md socket-free discovery & handshake
schemas/ control.avsc, FINGERPRINTS.md, example user schemas
connectors/
c/ native C connector (lib + header + tests)
cpp/ header-only C++ wrapper over the C core
python/ native Python connector (+ tiny atomics/futex extension)
go/ native Go connector (pure Go, no cgo)
ts/ TypeScript connector (Bun FFI over the C library)
# Terminal 1 — start the broker
cargo run -p impulsed
# Terminal 2 — run the demo connector (publish/subscribe + RPC)
cargo run -p impulse-ring-connector --example demoUsing the connector from Rust:
use impulse_ring_connector::Connection;
use std::time::Duration;
let conn = Connection::connect("my-service")?;
// Publish a channel (Avro schema is the contract; key gates subscribers).
let pubr = conn.publish_channel("metrics", METRIC_SCHEMA, Some("secret"))?;
pubr.publish(&metric)?;
// Subscribe (broker checks key + schema fingerprint).
let sub = conn.subscribe(channel_id, Some("secret"), METRIC_SCHEMA)?;
let m: Metric = sub.recv(Duration::from_secs(1))?.unwrap();
// Expose and call functions.
conn.expose_function::<Req, Resp, _>("add", REQ_SCHEMA, RESP_SCHEMA, None, |r| handle(r))?;
let resp: Resp = conn.call_blocking("add", None, &req, REQ_SCHEMA, RESP_SCHEMA, Duration::from_secs(5))?;cargo test --workspace # unit + integration (spawns the real broker)
cargo clippy --workspace --all-targets
cargo fmt --checkRegenerate the control schemas / fingerprints after a protocol change:
cargo run -p impulse-ring-core --example dump_schemas > spec/schemas/control.avsc
cargo run -p impulse-ring-core --example dump_schemas fps > spec/schemas/FINGERPRINTS.md- Rings are MPSC-safe via a futex mutex on the producer side; a lock-free SPSC fast path is a planned, wire-compatible optimization.
- One subscriber per channel; channel fan-out is a later milestone.
- Releasing a channel (
Unpublish) is implemented by the Rust connector. The C/C++/Go/Python/TS connectors do not send it yet, so a publisher there holds its channel — name and arena — until its connection closes. - A caller should resolve a function once and keep the answer (the Rust connector does); the other connectors look up per call, which puts the broker's single control thread on the hot path of every request.
- Heartbeat reaping is lenient; access keys use salted SHA-256 (argon2 later).
See spec/wire-format.md for the full contract.
MIT — see LICENSE.