HIBANA
hibana is a Rust 2024, #![no_std], no-alloc-oriented runtime for
affine multiparty session types.
It lets a protocol crate describe communication once as a global choreography,
project each participant into a compact local program, attach transport and
storage, and hand application code a small affine Endpoint.
The complete path is:
hibana::g choreography
-> integration::program::project(&program)
-> integration::runtime::Config::from_resources(...)
-> integration::SessionKitStorage::uninit().init()
-> kit.rendezvous(...)
-> registered rendezvous .session(...).role(...)
-> role witness .enter()
-> Endpoint
-> flow().send() / recv() / offer() / RouteBranch::decode()
There are only two public surfaces:
| Surface | Used by | Main names |
|---|---|---|
| Application surface | application code | hibana::g, Endpoint, RouteBranch, EndpointResult, EndpointError |
| Integration surface | protocol and integration crates | hibana::integration, hibana::integration::program |
If you are writing an application, stay on hibana::g and Endpoint. If you
are implementing a protocol crate, use hibana::integration to project, attach,
bind transport, install policy, and return endpoints.
Install
Add Hibana from crates.io:
Or write the dependency explicitly:
[]
= "0.8.0"
The default feature set is empty. Hibana is #![no_std] and no-alloc-oriented
by default.
Enable std only for host-side tests, diagnostics, and documentation builds:
[]
= { = "0.8.0", = ["std"] }
What Hibana Is
Hibana is for communication systems where the protocol shape should be known before runtime.
You write one global choreography:
use g;
let app = seq;
The choreography says:
- role
0sends message label1with au32payload to role1on lane0; - role
1then sends message label2with au32payload back to role0.
A protocol crate composes any required prefixes, projects the choreography for
each role, attaches transport and storage, and returns an Endpoint. The
application then drives only its local endpoint.
Affine Ownership, Not Shared Protocol State
Hibana's semantics are affine endpoint ownership and endpoint progress. The
current protocol state is the projected continuation owned by an Endpoint;
it is not a shared flag, shared table, shared memory cell, or ambient runtime
variable.
Each role must advance through its endpoint. The only evidence that may affect protocol progress is evidence admitted by the projected descriptor through the attached transport, or an explicit resolver decision at a projected route / loop policy point. Role code must not read shared memory, shared atomics, global flags, device registers, or side-channel state to decide whether a route is ready, a loop continues, or a message may be skipped.
Shared memory is especially not protocol authority. An integration crate may
use memory, atomics, interrupts, DMA, or OS primitives as private transport or
resolver implementation mechanics, but those mechanics must first become
transport frames, descriptor-checked binding evidence, or resolver inputs at
explicit policy points. They never replace flow().send(), recv(),
offer(), or RouteBranch::decode().
Quick Start
Application code usually sees an endpoint that a protocol crate has already attached.
use g;
endpoint.?.send.await?;
let reply = endpoint..await?;
That is the main user path:
- define messages and choreography with
hibana::g; - receive an attached
Endpointfrom your protocol crate; - call
flow().send(),recv(),offer(), andRouteBranch::decode().
flow() and offer() are previews. Endpoint progress happens when
flow().send(), recv(), or RouteBranch::decode() succeeds. A failed preview
does not move the endpoint and does not choose an alternate route. Preview
evidence can wake or guide polling, but it cannot mint a continuation.
Application Guide
Application authors only need these names:
hibana::g::{Msg, Program, send, seq, route, par}EndpointRouteBranchEndpointResult<T>EndpointError
The normal choreography language is:
use g;
let request = ;
let response = ;
let program = seq;
Keep choreography terms local. Compose them once and let the protocol crate
project them immediately. Program<S> is the unprojected typed choreography
term; RoleProgram<ROLE> is the projected runtime descriptor. Neither is a
transport handle, heap object, or reusable runtime object.
Sending And Receiving
Use flow().send() when the next local step is a send known from the
choreography:
endpoint
.?
.send
.await?;
Use recv() when the next local step is a deterministic receive:
let value = endpoint..await?;
The message type carries the choreography label, payload type, and optional control kind. The runtime checks the projected descriptor and fails closed if the label, lane, payload shape, or control/data kind does not match.
Routes
g::route(left, right) is binary. Branch labels must be unique within the
route shape.
use g;
use RouteDecisionKind;
let accepted = seq;
let rejected = seq;
let routed = route;
When the endpoint reaches a route decision, call offer():
let branch = endpoint.offer.await?;
match branch.label
If the chosen route arm begins with a send, drop the preview branch and send the first message in that arm:
let branch = endpoint.offer.await?;
match branch.label
The route is never selected by parsing payload bytes. Route authority comes from the projected descriptor or from an explicit resolver decision at a projected route point. Transport observation may only supply demux evidence that is checked against descriptor metadata; a frame label, payload shape, or binding hint is never an independent route decision.
Failure And Cancellation
Endpoint operations return EndpointResult<T>, so application code should use
ordinary ?:
endpoint.?.send.await?;
let reply = endpoint..await?;
let branch = endpoint.offer.await?;
let payload = branch..await?;
This shape has only two committed outcomes:
Ok(progress) next choreography state exists
Err(domain evidence) current session generation is terminal
Errors are not route arms. Transport close, decode failure, or protocol invariant failure poisons the affected session generation and returns diagnostic evidence. It does not authorize reconnect or a different branch in the same generation.
There is intentionally no recv_timeout, send_timeout, public cancel, or
same-generation recovery API. If time should select a branch, model time in the
choreography itself: use a timer or clock role and an explicit route point, then
install a resolver for that route.
Protocol-invisible liveness detection belongs inside the transport adapter. A
UDP, serial, or custom carrier that decides an I/O wait is terminal must return
TransportError from poll_send(...) or poll_recv(...); Hibana converts that
transport failure into terminal session evidence. Such watchdogs do not create
hidden route authority, carrier recovery policy, or same-generation recovery in Hibana.
The public evidence envelopes are domain-specific:
EndpointErrorforflow,send,recv,offer, anddecode;ResolverErrorfor resolver registration and resolver decisions;AttachErrorfor rendezvous and endpoint attach.
There is no public wide HibanaError, and public error-kind enums are not part
of the application decision surface. The Debug output records the operation
and callsite so top-level runners and panic handlers can report where a failure
was observed without requiring a second error layer at every call.
Parallel Composition
g::par(left, right) combines independent local flows. Empty arms and
overlapping (role, lane) ownership are rejected by projection.
use g;
let left = ;
let right = ;
let parallel = par;
Lanes are protocol-owned separation units. Application code should follow the
lane contract exposed by its protocol crate rather than assigning global lane
meaning inside hibana itself.
Payloads
Built-in exact codecs cover (), bool, integers, borrowed byte slices, and
fixed byte arrays. Fixed-width decoders reject trailing bytes.
Custom payloads implement hibana::integration::wire::WireEncode for sending
and hibana::integration::wire::WirePayload for receiving:
use ;
;
Decoded values may borrow from the received frame:
// In a message type, use `g::Msg<LABEL, &[u8]>`.
// The decoded value returned by recv/decode is borrowed from the endpoint
// transport frame currently owned by the endpoint.
Dynamic Policy
Dynamic policy is explicit. Mark the controller self-send that opens each
route or loop arm with Program::policy::<POLICY_ID>(), then let the
protocol crate install a resolver for that policy id. The policy annotation is
on the arm head, not on the g::route(...) call.
use g;
use RouteDecisionKind;
const POLICY_ID: u16 = 7;
let left =
.;
let right =
.;
let routed = route;
Policy does not appear as driver if/else logic. It is a choreography point
resolved through the integration policy seam.
If a resolver returns Defer while an offer is resolving a passive branch, the
offer remains pending unless new route evidence or a valid resolver decision
appears. If the controller is already attempting to send a route or loop control
message, Defer rejects that active attempt with PolicyAbort; an active send
does not park after the control frame has been selected. Hibana does not
maintain offer-time defer budgets, synthetic poll retries, progress-exhaustion
escape paths, or hidden deadline fuses.
Control Messages
Control messages are ordinary choreography messages. Public protocol-owned
controls are explicit wire tokens written as
g::Msg<LABEL, GenericCapToken<K>>, where K implements the
protocol-neutral WireControlKind trait. Endpoint-owned local minting is
crate-owned and exposed only through Hibana's built-in route/loop decision
kinds.
The message label is choreography identity. Control meaning comes from the control kind's descriptor metadata, not from reserved numeric labels.
RouteDecisionKind, LoopContinueKind, and LoopBreakKind are the built-in
local decision controls. They are how route arms and route-loop heads carry
explicit controller decisions without adding a second choreography language.
Program::policy::<ID>() is intentionally limited to these built-in decision
controls; custom protocol controls remain protocol-owned explicit wire effects
and do not become route or loop decision authority.
Protocol-owned wire controls use GenericCapToken<K> plus
WireControlKind. WireControlEffect decides the runtime effect; payload
contents, labels, transport hints, and driver if/else logic never become
route or transaction authority.
There are two public control layers:
GenericCapToken<K>plusWireControlKindis the choreography message shape for explicit wire control payloads.integration::cap::WireControlEffectis the protocol-visible effect set evaluated by the hibana control kernel.
The public wire effect catalogue is:
| Effect | Meaning | Usual use |
|---|---|---|
WireControlEffect::Fence |
Orders or authorizes a protocol-visible control boundary without changing topology or transaction state. | Protocol-owned explicit wire barrier. |
WireControlEffect::StateSnapshot |
Records the current session/lane generation before a mutation. | Snapshot before transaction, abort, restore, or topology-sensitive mutation. |
WireControlEffect::StateRestore |
Restores previously snapshotted state after a failed or aborted mutation. | Rollback path paired with StateSnapshot. |
WireControlEffect::TxCommit |
Commits a snapshot-backed transaction and finalizes that lane generation. | At-most-once commit of a protocol mutation. |
WireControlEffect::TxAbort |
Aborts a snapshot-backed transaction and records the abort path. | Fail-closed transaction cancellation. |
WireControlEffect::AbortBegin |
Starts an explicit abort handshake. | First step of a protocol-owned abort sequence. |
WireControlEffect::AbortAck |
Acknowledges an abort handshake. | Idempotent acknowledgement for abort completion. |
WireControlEffect::TopologyBegin |
Opens a topology transition intent with source/destination rendezvous, lane, and generation facts. | Distributed lane/rendezvous reconfiguration. |
WireControlEffect::TopologyAck |
Validates and acknowledges a topology intent at the destination side. | Destination half of topology coordination. |
WireControlEffect::TopologyCommit |
Commits an acknowledged topology transition and bumps generation. | Source-side topology finalization. |
These effects are not new application commands. A protocol that needs topology,
transaction, abort, snapshot, or fence control still writes ordinary
choreography messages, usually with a protocol-owned WireControlKind that
maps to the relevant WireControlEffect. The runtime consumes projected
descriptor metadata fail-closed.
Explicit wire controls always use the public wire path and reusable descriptor
semantics. Local route/loop decisions stay Hibana-owned and are exposed only as
the built-in RouteDecisionKind, LoopContinueKind, and LoopBreakKind.
Topology and transaction control are integration-level tools. Use them only when the protocol itself needs a choreography-visible transition:
- topology: move or rebind a lane/rendezvous relation with
TopologyBegin -> TopologyAck -> TopologyCommit; - transaction: bracket a multi-step mutation with
StateSnapshot -> TxCommitorStateSnapshot -> TxAbort/StateRestore; - abort: make cancellation explicit with
AbortBegin -> AbortAck; - fence: insert a protocol-owned ordering or readiness boundary without adding domain-specific APIs to hibana core.
Do not add g::topology, g::tx, driver-side repeat loops, or payload-driven
branch selection. The authority source remains the choreography plus the
projected descriptor.
Custom wire controls name the message label separately from control metadata:
use g;
use ;
const CUSTOM_WIRE_MSG_LABEL: u8 = 200;
;
type CustomWireMsg =
Msg CUSTOM_WIRE_MSG_LABEL }, >;
Use the built-in RouteDecisionKind, LoopContinueKind, and LoopBreakKind
with () payloads for local route/loop decisions. Use an explicit
GenericCapToken<K> payload for protocol-owned wire controls. Explicit wire
controls use reusable descriptor semantics; Hibana does not mint or register their token bytes.
Protocol Integration
Protocol crates use the same hibana::g language as applications. There is
no second composition language.
Compose And Project
A protocol crate may place transport or integration prefixes before the application choreography, then project each role.
use g;
use ;
let prefix = seq;
let app = seq;
let program = seq;
let client: = project;
let server: = project;
project(&program) is the projection boundary. Runtime code consumes the
projected descriptor; it does not rediscover protocol shape.
Generated protocol packages and composition facades may hide the concrete
Program<_> step-list type when returning an unnamed choreography value. They
return impl integration::program::Projectable, and callers still use the same
project(&program) entry. Projectable is a sealed choreography bound, not a
second choreography language and not a runtime authority. It has no
runtime-universe type parameter; facade runtimes keep their universe on their
own storage/configuration types, not on the choreography projection bound.
Attach An Endpoint
The canonical integration path is borrowed and caller-provided:
use integration;
use SessionId;
use ;
let mut tap_buf = ;
let mut slab = ;
let clock = new;
let mut kit_storage = uninit;
let kit = kit_storage.init;
let config = from_resources;
let rv = kit.rendezvous?;
let endpoint = rv.session.role.enter?;
SessionKitStorage::init() is the only public construction path. It writes the
kit in place into caller-owned storage, returns the stable borrow used
by endpoint attach, and drops the initialized kit exactly once. The raw unsafe
initializer and MaybeUninit protocol are not part of the public surface.
Config::from_resources owns the rendezvous storage and clock authority. Lane
domain and endpoint lease capacity are not caller-selected config. A fresh
rendezvous starts with no materialized lane storage and no endpoint lease table.
Role attach reads the projected descriptor, grows exactly the lane
tables and endpoint lease entries it needs, and preserves existing session state
if a later projected role needs a wider lane span. Integration code must not
pass caller-chosen lane windows, endpoint counts, or deadline knobs.
The protocol crate owns concrete MyTransport and any binding state. The
application receives only Endpoint.
Useful integration owners:
integration::program::{project, RoleProgram}integration::SessionKitintegration::runtime::{Config, CounterClock, DefaultLabelUniverse, LabelUniverse, RING_EVENTS}integration::ids::{EffIndex, Lane, SessionId}integration::transport::Transportintegration::binding::{BindingError, EndpointSlot, Channel, IngressEvidence}integration::policy::{ResolverError, ResolverRef, DecisionArm, DecisionResolution}integration::wire::{Payload, WireEncode, WirePayload}integration::cap::{GenericCapToken, WireControlKind, WireControlEffect}integration::runtime::TapEvent
Built-in route/loop decision kinds live under integration::cap::control.
Transport
Implement integration::transport::Transport to connect Hibana to an I/O
system. The transport sees bytes, frame labels, and readiness; it does not own
choreography meaning, route authority, policy inputs, telemetry, or application
cancellation semantics.
Protocol-invisible carrier watchdogs belong inside poll_send(...) and
poll_recv(...): if the transport concludes that progress is impossible, it
returns TransportError and Hibana terminates the current session generation.
The transport owns:
open(port)for the descriptor-derived role/session/lane port witness;poll_send(...)andpoll_recv(...);cancel_send(...)for transport cleanup when a send future is dropped after staging carrier state;requeue(...)as the required rollback path for a frame that descriptor checks cannot commit.
open(port) returns Tx/Rx handles whose lifetime is bound to the transport
borrow, so an embedded carrier can keep buffers, wakers, and DMA bookkeeping
inside the transport owner without allocating or exporting a separate context.
The only optional transport hook is recv_frame_hint(...), a non-blocking
route-observation hint-drain. It must not consume payload bytes. Once it yields
a frame label, it must not yield the same observation again until
poll_recv(...) or requeue(...) stages fresh receive state.
Binding
Use enter() when the transport can deliver the next payload directly.
Use EndpointSlot when the integration demuxes ingress into binding-owned
payload handles. IngressEvidence is demux evidence only. It may support
descriptor-checked route observation, but it is not an independent route
decision and must not be used as dynamic route authority without resolver
authority. Attach those integrations with role(...).binding(slot).enter();
enter() remains the only endpoint attach operation.
A binding slot returns IngressEvidence for a lane and later reads from the
selected handle:
Resolver Policy
Resolvers are installed by the protocol crate for explicit policy points.
Route and loop control messages use the same decision vocabulary; loop is not
a separate user-facing resolver API. Resolver state is the policy input owner:
use ResolverRef::decision_state(...) when a resolver needs protocol-specific
observations. Resolver failure rejects the step; it does not fall through to a
different semantic path.
Guarantees
Hibana keeps the public API small because the projection boundary carries the proof work.
Core guarantees:
- Rust 2024 and stable Rust
1.95; - default features are empty;
- runtime code is
no_stdand no-alloc-oriented; - descriptor storage is caller-provided, borrowed, static, or slab-backed;
- route shape, duplicate labels, malformed control paths, and lane ownership errors are rejected before endpoint execution;
- runtime cursor progress is one-way and affine;
- protocol state is affine endpoint ownership, not shared atomic or shared memory state;
- failed sends, receives, offers, and decodes do not authorize hidden progress;
- payload decode is exact;
- message logical labels and transport frame labels are separate concepts;
- control semantics are descriptor metadata, not reserved numeric labels;
- route authority is limited to projected facts and explicit resolver decisions; descriptor-checked transport observation may only confirm or demux projected facts.
What application code should not do:
- call transport APIs directly from localside logic;
- choose route arms by parsing payloads;
- model dynamic policy as driver-side branching;
- treat binding hints or frame labels as route authority;
- match endpoint errors to continue the same generation on a hidden alternate path;
- use shared memory, shared atomics, global flags, or side-channel state as route readiness, loop-control, or protocol-progress authority;
- expose protocol-specific APIs through the
hibanacrate root.
Validation
For a published crate consumer, the useful checks are ordinary Cargo commands:
The full test suite is repository-only; it depends on source-tree fixtures that are intentionally excluded from the production crate package.
For a repository checkout, maintainers should run the repository gate suite before release:
Use that gate rather than raw cargo test; repo-only unit tests are enabled
through hibana_repo_tests. The suite protects the public surface, no_std build,
projection boundary, descriptor publication, future layout, route authority, and
size measurements. It is intentionally kept outside the crate package.