Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
rusteron-client
rusteron-client is a core component of the Rusteron project.
It provides a Rust wrapper around the Aeron C client API, enabling high-performance, low-latency communication in distributed systems built with Rust.
This crate supports publishing, subscribing, and managing Aeron resources, while exposing a flexible and idiomatic interface over unsafe C bindings.
Due to its reliance on raw FFI, developers must take care to manage resource lifetimes and concurrency correctly.
Sponsored by GSR
Rusteron is proudly sponsored and maintained by GSR, a global leader in algorithmic trading and market making in digital assets.
It powers mission-critical infrastructure in GSR's real-time trading stack and is now developed under the official GSR GitHub organization as part of our commitment to open-source excellence and community collaboration.
We welcome contributions, feedback, and discussions. If you're interested in integrating or contributing, please open an issue or reach out directly.
Features
- Client Setup – Create and start an Aeron client using Rust.
- Publications – Send messages via
offer()ortry_claim(). - Subscriptions – Poll for incoming messages and handle fragments.
- Callbacks & Handlers – React to driver events like availability, errors, and stream lifecycle changes.
- Cloneable Wrappers – All client types are cloneable and share ownership of the underlying C resources.
- Automatic Resource Management – Objects created with
.new()automatically call*_initand*_close, where supported. - Result-Focused API – Methods returning primitive C results return
Result<T, AeronCError>for ergonomic error handling. - Efficient String Interop – Inputs use
&CStr, outputs return&str, giving developers precise allocation control.
Installation
Add rusteron-client to your Cargo.toml:
# Dynamic linking (default)
= "0.2"
# Static linking
= { = "0.2", = ["static"] }
# Static linking with precompiled C libraries (best for Mac users, no Java/cmake needed)
= { = "0.2", = ["static", "precompile"] }
# Static linking with precompiled C libraries using rustls downloader
= { = "0.2", = ["static", "precompile-rustls"] }
When using the default dynamic configuration, you must ensure Aeron C libraries are available at runtime. The static option embeds them automatically into the binary.
General Patterns
new()Initialization: Automatically calls the corresponding*_initmethod.- One-liner connect:
Aeron::connect(Some(dir))?builds the context, client, and starts the conductor; build theAeronContextyourself for tuned setups. (AeronArchive::connect(...)in rusteron-archive.) - Retained images auto-release:
subscription.image_at_index(i)/image_by_session_id(id)returnOption<AeronImage>that releases back to the subscription on drop;for_each_image(|img| …)borrows without bookkeeping. - Automatic Cleanup (Partial): When possible,
Dropwill invoke the appropriate*_closeor*_destroymethods. - Manual Resource Responsibility: For methods like
set_aeron()or where lifetimes aren't managed internally, users are responsible for safety. - Handlers Are Reference-Counted: Wrap callbacks with
Handler::new(...). Methods that register a callback the C client retains keep a clone alive inside the registering resource, so the value is freed automatically — no manualrelease().
Handlers and Callbacks
Two kinds of callbacks:
1. Retained (error handler, image/counter lifecycle, notifications)
The C client stores these and invokes them later from the conductor thread. Setters take the
callback by value (a closure or any impl Trait), heap-allocate it into a reference-counted
Handler, and keep a clone inside the registering resource — it is freed when that resource
drops, no manual release():
ctx.set_error_handler?;
Keep the returned Handler to read the callback's state later:
let counts = ctx.set_error_handler?.unwrap;
// ... later
println!;
Registration methods that return a resource (e.g. add_subscription with image handlers) take
Option<&Handler<T>> and clone it into the resource — same lifetime guarantee.
2. Synchronous (poll, controlled_poll, log readers)
Invoked only during the call, so they take a stack closure — zero allocation, may borrow local state:
subscription.poll_fn?;
For messages larger than the MTU, wrap the delegate in an AeronFragmentAssembler (it
reassembles fragments before calling back) — poll_fn delivers raw fragments
only. The ergonomic wrapper is AeronFragmentClosureAssembler; its poll borrows a &mut T
context for the call (the callback is a fn pointer, not a closure, so pass state through
the context):
use ;
let mut assembler = new?;
let mut stats = Stats ;
assembler.poll?; // 10 = fragment limit
// stats.bytes now holds the reassembled payload sizes
No callback for an optional slot? Handlers::NONE fits any callback parameter.
Building channel URIs
Prefer the typed [AeronUriStringBuilder] over hand-written URI strings — parameters are
typed setters, so misspelled keys and malformed values are caught before they reach the
driver:
let channel = udp?.build?;
let mds_sub = udp_control?.build?;
let response = udp_control?
.response_correlation_id?
.build?;
Constructors: ipc(), udp(endpoint), udp_control(control, ControlMode); plus ~50 typed
setters (session_id, mtu_length, term_length, fc, gtag, reliable, …). See
examples/multi_destination_subscription.rs and examples/request_response.rs.
Minimal Pub/Sub
use *;
use Duration;
let ctx = new?;
// Reuse the built-in logger for async client errors (Aeron samples always set one).
ctx.set_error_handler?;
let aeron = new?;
aeron.start?;
let channel = c"aeron:ipc"; // compile-time &CStr, zero runtime cost
let publication = aeron
.async_add_publication?
.poll_blocking?;
let subscription = aeron
.async_add_subscription?
.poll_blocking?;
// offer() returns Ok(position) or a typed AeronOfferError — see "Errors & offer results".
loop
subscription.poll_fn?;
Note on
poll_blocking/add_*(.., timeout): these block the calling thread in a busy-poll loop and exist for example/test brevity. In production, drive the async poller'spoll()from your own event loop instead. Seeexamples/non_blocking_publisher.rsfor the idiomatic pattern.
AddressSanitizer
The sanitize-address feature compiles the Aeron C sources with -fsanitize=address
(implies build-from-source), so use-after-free / double-free across the FFI boundary are
caught in your tests:
# macOS — stable rustc; Apple's ASan runtime is linked automatically, it just needs to be
# on the loader path:
ASAN_OPTIONS=detect_leaks=0
# Linux — nightly, additionally instruments the Rust side:
RUSTFLAGS="-Zsanitizer=address"
Notes: leak detection is noisy with a media driver in-process — keep detect_leaks=0;
other sanitizers (thread, undefined) can be selected with the RUSTERON_SANITIZE
env var instead of the feature. In this repo, just test-asan runs the client suite this way.
Resource Lifecycle & close()
The C client owns every child resource (publications, subscriptions, counters) and frees them
all in aeron_close. rusteron therefore reference-counts teardown:
aeron.close()(anddrop) is deferred while any child handle or clone is alive — the surviving handles remain fully usable; the C close runs when the last reference releases.- Leaf resources (
publication.close()etc.) close immediately; clones of that handle see a nulled pointer and become inert. unsafe aeron.close_now()forces the C close immediately. Every surviving child handle then dangles — even itsDropis a double free — somem::forgetsurvivors or never return.
Which structures get which behaviour:
| Behaviour | Types |
|---|---|
Deferred close() + unsafe close_now() |
Aeron, AeronArchive (rusteron-archive) |
Immediate close(self) (+ close_with_handler for a close-complete notification); clones become inert |
AeronPublication, AeronExclusivePublication, AeronSubscription, AeronCounter |
| No public close — freed when the last reference drops (the client holds an internal clone, so they always outlive it) | AeronContext, AeronArchiveContext, AeronDriverContext |
No close at all — the C client frees them when their poll() completes (created, errored, or cancelled) |
AeronAsyncAdd* pollers, AeronAsyncDestination |
Ordering is structurally safe in any combination: children hold references to the client, the
client holds its context, and closing/dropping in any order defers the C teardown until the
last handle releases (verified by the complex_object_graph_close_is_safe_in_any_order test).
Migrating 0.1 → 0.2
Breaking changes, made because the old design allowed double frees and use-after-free (see PR #50):
| 0.1 | 0.2 |
|---|---|
Handler::leak(h) + manual release() |
Handler::new(h); freed automatically (Arc-counted, resources keep clones) |
ctx.set_error_handler(Some(&handler)) |
ctx.set_error_handler(Some(h)) — takes the value (closures work), returns the Handler |
set_error_handler_fn(closure) (UB: C retained a stack closure) |
removed; _once now exists only for synchronous callbacks |
aeron.close() freed children immediately (UAF on surviving handles) |
deferred until the last reference drops; unsafe close_now() is the escape hatch |
Handler was Sync |
Send only (conductor thread may invoke callbacks) |
offer(buf, supplier) returned a raw i64 sentinel |
offer(buf) → Result<i64, AeronOfferError> with is_retryable(); supplier form: offer_with_reserved_value; raw sentinel: offer_raw |
offer_result / offer_result_simple / try_claim_result (→ AeronCError) |
offer_with_reserved_value / offer / try_claim (→ typed AeronOfferError) |
poll_fn(f, limit) |
poll_fn(f, limit) — for_each_fragment removed |
Handlers::no_xxx_handler() per callback |
Handlers::NONE for any callback parameter |
The full old → new table (destinations, fragment assembler, C strings, driver guard) lives in the root README's migration guide.
Examples
examples/basic_pub_sub.rs— minimal pub/sub with fragment assemblyexamples/streaming_rate.rs— streaming publisher + rate-reporting subscriber (port ofstreaming_publisher.c+rate_subscriber.c)examples/multi_destination_subscription.rs— MDS: one manual-control subscription aggregating several endpoints (port ofbasic_mds_subscriber.c)examples/driver_stats.rs— CnC tooling: counters, distinct error log, loss report (ports ofaeron_stat.c/error_stat.c/loss_stat.c)examples/embedded_ping_pong.rs— RTT ping/pong withtry_claim(port ofcping/cpong)examples/embedded_exclusive_ipc_throughput.rs— exclusive-publication IPC throughputexamples/request_response.rs— response channels (aeron 1.44+): request/response wiring viacontrol-mode=response+response-correlation-id(port ofresponse_server.c/response_client.c)examples/file_transfer.rs— chunked file transfer with fragment reassembly and verification (port ofFileSender/FileReceiver)
Documentation & Guides
For detailed guides and code snippets on Aeron features in Rust, see:
Errors & offer results
- Client errors: install an error handler on the context (
ctx.set_error_handler(Some(handler))) so async errors aren't silently lost — Aeron's samples always do. offer()/try_claim()results:Result<i64, AeronOfferError>—Okis the new log position; the error is a typed sentinel withis_retryable()(back-pressured, admin action, not connected — retry, ideally with an idle strategy) vs fatal (closed, max position exceeded). This mirrors Aeron'sBasicPublisher.checkResultwithout magic numbers:
For branch-free hot paths,match publication.offeroffer_raw()/try_claim_raw()return the rawi64sentinel. The typed path costs the same on the happy path — the error enum only materialises on the error path.- Image handlers:
Handlers::NONEfor each image slot is a fine default, but real apps usually react to image availability (logging, synchronisation) — Aeron'sPingsample uses one as a latch.
Idle strategies
Poll loops should back off when a cycle does no work. Rusteron ports Aeron's IdleStrategy
(idle(work_count) returns immediately when work was done, otherwise backs off):
use ;
let mut idle = new; // spin → yield → sleep, Aeron's default
loop
Available: [BusySpinIdleStrategy] (lowest latency, pins a core), [YieldingIdleStrategy],
[SleepingIdleStrategy] (fixed sleep), [BackoffIdleStrategy] (adaptive, general-purpose),
[NoOpIdleStrategy]. Latency benchmarks should keep busy-spin.
BackoffIdleStrategy is the default and matches Aeron's C/Java backoff exactly (10 spins,
20 yields, park 1µs..1ms), so there is no need for a C-backed idle strategy to get parity.
The conductor's own idle strategy is configured on the context — use the typed enum
(ctx.set_idle_strategy_kind(AeronIdleStrategyKind::Backoff)?, which also sets coherent
init args) — and likewise the media driver's per-agent strategies
(set_conductor_idle_strategy_kind etc. on AeronDriverContext).
For recording, replay, and persistent subscriptions (replay history, then seamlessly join a live stream), see rusteron-archive.
Contributing & License
See the root README and CONTRIBUTING.md. Dual-licensed under MIT or Apache-2.0.
Acknowledgments
Special thanks to:
- @mimran1980, a core low-latency developer at GSR and the original creator of Rusteron - your work made this possible!
- @bspeice for the original
libaeron-sys - The Aeron community for open protocol excellence