hibana is a Rust 2024 #![no_std] / no_alloc oriented affine MPST runtime.
It has two public surfaces:
- App surface:
hibana::gplusEndpoint - Substrate surface:
hibana::g::advancedplushibana::substrate
Application code should stay on hibana::g and Endpoint. Protocol crates
use hibana::g::advanced and hibana::substrate to attach transport,
binding, resolver, and policy.
Everything else is lower layer.
hibana is for protocols that want one global choreography as the source of
truth and a small localside API at runtime.
The normal flow is:
- Write one choreography with
hibana::g - Let a protocol crate compose transport and appkit prefixes around it
- Receive an attached
Endpoint - Advance that endpoint with
flow().send(),recv(),offer(), anddecode()
What hibana gives you:
- typed projection from one global program
- compile-time checks for route shape, lane ownership, and composition errors
- fail-closed localside execution for label and payload mismatches
- a protocol-neutral core that does not bake QUIC, HTTP/3, or other wire terms into the crate root
std- enables transport/testing utilities and observability normalisers. The runtime remains slab-backed andno_allocoriented in both modes.
Write a choreography once with hibana::g:
use hibana::g;
let request_response = g::seq(
g::send::<g::Role<0>, g::Role<1>, g::Msg<1, u32>, 0>(),
g::send::<g::Role<1>, g::Role<0>, g::Msg<2, u32>, 0>(),
);After integration code returns an attached endpoint, app code stays on the localside core API:
use hibana::g;
let _sent = endpoint.flow::<g::Msg<1, u32>>()?.send(&7).await?;
let reply = endpoint.recv::<g::Msg<2, u32>>().await?;
let _ = reply;That is the intended app path: define a choreography, receive an endpoint, and drive it with the localside methods.
App authors should stay on hibana::g and Endpoint.
| Job | Public owner |
|---|---|
| Define choreography | hibana::g::{send, route, par, seq} |
| Mark a dynamic policy point | Program::policy::<POLICY_ID>() |
| Send a known message | flow().send() |
| Receive a known message | recv() |
| Observe a route choice | offer() |
| Receive the first message of the chosen route arm | decode() |
| Inspect a chosen branch | RouteBranch::{label, decode} |
App code does not call projection, attach, binding, transport, or resolver setup directly.
The public app language is fixed to:
hibana::g::{send, route, par, seq}Program::policy::<POLICY_ID>()RouteBranch::label()RouteBranch::decode()flow().send()offer()recv()decode()
g::route(left, right) is binary. g::par(left, right) is also binary and
requires disjoint (role, lane) ownership.
use hibana::g;
let left_arm = g::seq(
g::send::<g::Role<0>, g::Role<0>, g::Msg<10, ()>, 0>().policy::<7>(),
g::send::<g::Role<0>, g::Role<1>, g::Msg<11, [u8; 4]>, 0>(),
);
let right_arm = g::seq(
g::send::<g::Role<0>, g::Role<0>, g::Msg<12, ()>, 0>().policy::<7>(),
g::send::<g::Role<0>, g::Role<1>, g::Msg<13, u16>, 0>(),
);
let routed = g::route(left_arm, right_arm);
let parallel = g::par(
g::send::<g::Role<0>, g::Role<1>, g::Msg<20, u32>, 1>(),
g::send::<g::Role<0>, g::Role<1>, g::Msg<21, [u8; 8]>, 2>(),
);
let app = g::seq(routed, parallel);Rules worth remembering:
- annotate only the control point that actually needs dynamic policy
- duplicate route labels are compile errors
- empty
g::pararms are compile errors - overlapping
(role, lane)pairs ing::parare rejected - dynamic route does not silently appear at runtime; it must be explicit in the choreography
Each localside method has one job:
flow().send()for sends you already know staticallyrecv()for deterministic receivesoffer()when the next step is a route decisiondecode()when the chosen arm begins with a receivedrop(branch); endpoint.flow().send()when the chosen arm begins with a send
use hibana::g;
let outbound = endpoint.flow::<g::Msg<1, u32>>()?.send(&7).await?;
let inbound = endpoint.recv::<g::Msg<2, u32>>().await?;
let mut branch = endpoint.offer().await?;
match branch.label() {
30 => {
let payload = branch.decode::<g::Msg<30, [u8; 4]>>().await?;
let _ = (payload, outbound, inbound);
}
31 => {
drop(branch);
let () = endpoint.flow::<g::Msg<31, ()>>()?.send(&()).await?;
}
_ => unreachable!(),
}The app-facing owners at the crate root are:
hibana::SendResult<T>hibana::RecvResult<T>hibana::SendErrorhibana::RecvErrorhibana::RouteBranch
The public surface stays small because guarantees are pushed into the type system:
- projection stays typed through
RoleProgram<'prog, ROLE, Mint>and defaults toRoleProgram<'prog, ROLE> g::routerejects duplicate labels and controller mismatches before runtimeg::parrejects empty fragments and role/lane overlap before runtime- localside runtime is fail-closed for label and payload mismatches
hibana is choreography-first.
The connection shape is always:
transport prefix -> appkit prefix -> user app
On the choreography side, that means:
g::seq(transport prefix, g::seq(appkit prefix, APP))
The intended split is:
- App code writes
APP: g::Program<_> - A protocol crate composes transport and appkit prefixes around
APP - The protocol crate projects a typed role witness with
project(&PROGRAM) - The protocol crate attaches transport, binding, and policy, then returns the first app endpoint
- App code continues only through the localside core API
This keeps the user-facing path small while preserving typed projection and a protocol-neutral core.
- The driver follows
offer(); it does not invent decisions on its own - Branch handling is just
match branch.label() - Use
branch.decode()when the chosen arm begins with a receive - Use
drop(branch); endpoint.flow().send()when the chosen arm begins with a send flow()andoffer()are preview-only; endpoint progress happens only whensend()ordecode()successfully consumes the preview- App code and generic driver logic do not call transport APIs directly
Route authority has exactly three public sources:
Ackfor already materialized canonical control decisionsResolverfor dynamic-route resolutionPollfor transport-observable static evidence
Important negative rule: hint labels and binding classifications are demux or readiness evidence, not a fourth authority source.
Loop meaning comes from metadata, not from special wire labels. Labels are representation; they are not semantic authority on their own.
hibanaの app surface では、lane の意味は固定されません- app lane ownership は protocol / appkit contract が決めます
- canonical な attach path では control traffic を lane
0に載せます - additional reserved lanes があるかどうかは transport / appkit 側の契約です
- bindings own demux and channel resolution, not route authority
hibanaruntime が app lane を推測したり吸収したりはしません- unknown lanes are errors once the binding / transport contract is fixed
PolicySignalsProvider::signals(slot)is the single public slot-input boundary- EPF executes inside the resolver slot; it is not a second public policy API
- fail-closed is the default for verifier, trap, or fuel failures
- policy distribution and activation belong to the integration layer, not to endpoint-local helpers
| Layer | Writes | Reads |
|---|---|---|
| Transport | yes | yes |
| Resolver | no | yes |
| EPF | no | yes |
| Binder | no | yes |
| Driver | no | no |
| Surface | Who uses it | Main owners |
|---|---|---|
| App surface | application code | hibana::g, Endpoint, RouteBranch, SendResult, RecvResult |
| Substrate surface | protocol implementations | hibana::g::advanced, hibana::substrate |
If a concept is not owned by one of the two surfaces above, treat it as lower layer rather than as part of the app-facing contract.
Protocol implementations use the same choreography language as applications. There is no second composition DSL.
use hibana::g;
use hibana::g::advanced::{RoleProgram, project};
const PROGRAM: g::Program<_> =
g::seq(TRANSPORT_PREFIX, g::seq(APPKIT_PREFIX, APP));
let client: RoleProgram<'_, 0> = project(&PROGRAM);Protocol implementors use the protocol-neutral SPI:
hibana::gowns choreography compositionhibana::g::advancedowns typed projection and compile-time control-message typinghibana::substrateowns attach, enter, binding, resolver, policy, and transport seams- the root app surface does not expose
SessionKit,BindingSlot,RoleProgram, or typestate internals
The everyday protocol-side owners are:
hibana::g::advanced::{project, RoleProgram, CanonicalControl, ExternalControl, MessageSpec, ControlMessage, ControlMessageKind}hibana::substrate::SessionKithibana::substrate::{AttachError, CpError, EffIndex, Lane, RendezvousId, SessionId}hibana::substrate::Transporthibana::substrate::binding::{BindingSlot, NoBinding}hibana::substrate::policy::{ContextId, ContextValue, DynamicResolution, PolicyAttrs, PolicySignals, PolicySignalsProvider, ResolverContext, ResolverError, ResolverRef, PolicySlot}hibana::substrate::runtime::{Clock, Config, CounterClock, DefaultLabelUniverse, LabelUniverse}hibana::substrate::tap::TapEventhibana::substrate::cap::{CapShot, ControlResourceKind, GenericCapToken, Many, One, ResourceKind}hibana::substrate::wire::{Payload, WireEncode, WirePayload}hibana::substrate::transport::{Outgoing, TransportError, TransportEvent, TransportEventKind, TransportSnapshot}
Advanced buckets:
hibana::substrate::policy::core::*for fixed context-key idshibana::substrate::cap::advancedfor mint details and the built-in control-kind cataloguehibana::substrate::transportdetail owners for send direction, algorithm reporting, and metrics translation
Everything in this section is protocol-neutral. If a concept is protocol
specific, keep it outside hibana's public surface.
There is no public g::splice, g::delegate, or g::reroute.
delegate, splice, reroute, route, loop, and management-policy
operations are all expressed as ordinary g::send() steps whose message type
carries a capability token and a control-handling marker.
The key owners are:
CanonicalControl<K>for locally minted control tokensExternalControl<K>for control tokens carried on the wireMessageSpecfor label, payload, and control typingControlMessageandControlMessageKindfor control-message-only contracts
Handling rules are fixed:
CanonicalControl<K>is compile-time restricted to self-sendExternalControl<K>may cross roles and ride the wire- the operation itself comes from the control kind's resource tag, not from a second DSL
The built-in control kind catalogue lives under
hibana::substrate::cap::advanced:
- route and loop:
RouteDecisionKind,LoopContinueKind,LoopBreakKind - checkpoint and recovery:
CheckpointKind,CommitKind,RollbackKind,CancelKind,CancelAckKind - splice and reroute:
SpliceIntentKind,SpliceAckKind,RerouteKind - policy lifecycle:
PolicyLoadKind,PolicyActivateKind,PolicyRevertKind,PolicyAnnotateKind - management load protocol:
LoadBeginKind,LoadCommitKind
If a protocol needs a custom control kind, implement the capability traits in
hibana::substrate::cap::advanced and carry that kind through
GenericCapToken<K> plus CanonicalControl<K> or ExternalControl<K>.
Control kinds are still just message types in the choreography:
use hibana::g;
use hibana::g::advanced::{CanonicalControl, ExternalControl};
use hibana::substrate::cap::{ControlResourceKind, GenericCapToken};
use hibana::substrate::cap::advanced::{
LoopContinueKind, SpliceIntentKind,
};
let loop_continue = g::send::<
g::Role<0>,
g::Role<0>,
g::Msg<
{ <LoopContinueKind as ControlResourceKind>::LABEL },
GenericCapToken<LoopContinueKind>,
CanonicalControl<LoopContinueKind>,
>,
0,
>();
let splice_intent = g::send::<
g::Role<0>,
g::Role<1>,
g::Msg<
{ <SpliceIntentKind as ControlResourceKind>::LABEL },
GenericCapToken<SpliceIntentKind>,
ExternalControl<SpliceIntentKind>,
>,
0,
>();Capability-building owners live in two layers:
hibana::substrate::cap::{One, Many}for affine shot disciplinehibana::substrate::cap::{CapShot, ResourceKind, ControlResourceKind}for runtime capability representationhibana::substrate::cap::advanced::{MintConfig, MintConfigMarker, ControlMint, SessionScopedKind, AllowsCanonical, EpochTbl, CAP_HANDLE_LEN, CapError, CapsMask}for mint configurationhibana::substrate::cap::advanced::{ControlScopeKind, ScopeId, ControlHandling}for control-scope metadata
hibana::substrate::Transport is the protocol-neutral I/O seam. It owns send,
recv, requeue, event draining, hint exposure, metrics, and pacing updates.
Send and Recv future types must be Unpin.
struct MyTransport;
impl hibana::substrate::Transport for MyTransport {
type Error = hibana::substrate::transport::TransportError;
type Tx<'a> = () where Self: 'a;
type Rx<'a> = () where Self: 'a;
type Send<'a> = core::future::Ready<Result<(), Self::Error>> where Self: 'a;
type Recv<'a> =
core::future::Ready<Result<hibana::substrate::wire::Payload<'a>, Self::Error>>
where
Self: 'a;
type Metrics = ();
fn open<'a>(&'a self, _local_role: u8, _session_id: u32) -> (Self::Tx<'a>, Self::Rx<'a>) {
((), ())
}
fn send<'a, 'f>(
&'a self,
_tx: &'a mut Self::Tx<'a>,
_outgoing: hibana::substrate::transport::Outgoing<'f>,
) -> Self::Send<'a>
where
'a: 'f,
{
core::future::ready(Ok(()))
}
fn recv<'a>(&'a self, _rx: &'a mut Self::Rx<'a>) -> Self::Recv<'a> {
static EMPTY: [u8; 0] = [];
core::future::ready(Ok(hibana::substrate::wire::Payload::new(&EMPTY)))
}
fn requeue<'a>(&'a self, _rx: &'a mut Self::Rx<'a>) {}
fn drain_events(
&self,
emit: &mut dyn FnMut(hibana::substrate::transport::TransportEvent),
) {
emit(hibana::substrate::transport::TransportEvent::new(
hibana::substrate::transport::TransportEventKind::Ack,
10,
1200,
0,
));
}
fn recv_label_hint<'a>(&'a self, _rx: &'a Self::Rx<'a>) -> Option<u8> {
None
}
fn metrics(&self) -> Self::Metrics {
()
}
fn apply_pacing_update(&self, _interval_us: u32, _burst_bytes: u16) {}
}Transport rules:
recv()must yield borrowed payload viewsrecv()anddecode()return the decoded view chosen by the payload ownerrequeue()is how transport hands an unconsumed frame backdrain_events()feeds protocol-neutral transport observationrecv_label_hint()is a demux hint, not route authoritymetrics()returnsTransportSnapshotthroughTransportMetrics
hibana::substrate::SessionKit::new(&clock) is the canonical starting point.
The borrowed, no_alloc-oriented path is the canonical substrate path: keep
the clock, config storage, projected program, and resolver state borrowed; add
rendezvous once; then enter().
let mut tap_buf = [hibana::substrate::tap::TapEvent::zero(); 128];
let mut slab = [0u8; 64 * 1024];
let clock = hibana::substrate::runtime::CounterClock::new();
let config = hibana::substrate::runtime::Config::new(&mut tap_buf, &mut slab);
let cluster: hibana::substrate::SessionKit<
'_,
MyTransport,
hibana::substrate::runtime::DefaultLabelUniverse,
hibana::substrate::runtime::CounterClock,
4,
> = hibana::substrate::SessionKit::new(&clock);
let transport = MyTransport;
let rv_id = cluster.add_rendezvous_from_config(config, transport)?;
let endpoint = cluster.enter(
rv_id,
hibana::substrate::SessionId::new(1),
&CLIENT,
hibana::substrate::binding::NoBinding,
)?;Key points:
- the borrowed path is canonical even under
std - storage stays slab-backed, not heap-backed
- the same borrowed
RoleProgramcan be passed toset_resolver()andenter() Config::new(tap_buf, slab)allocates tap storage and the rendezvous slabConfig::with_lane_range(range)reserves lane space for the transport/appkit splitConfig::with_universe(universe)andConfig::with_clock(clock)install custom label-universe and clock owners- bootstrap failures use
hibana::substrate::CpErrorandhibana::substrate::AttachError
BindingSlot is the transport-adapter seam for framed streams, multiplexed
channels, and slot-scoped policy signals. It is also where protocol code
supplies PolicySignalsProvider.
BindingSlot is demux and transport observation only. It does not decide route
arms.
struct MyBinding {
signals: hibana::substrate::policy::PolicySignals,
}
impl hibana::substrate::policy::PolicySignalsProvider for MyBinding {
fn signals(
&self,
_slot: hibana::substrate::policy::PolicySlot,
) -> hibana::substrate::policy::PolicySignals {
self.signals
}
}
impl hibana::substrate::binding::BindingSlot for MyBinding {
fn poll_incoming_for_lane(
&mut self,
_logical_lane: u8,
) -> Option<hibana::substrate::binding::IncomingClassification> {
Some(hibana::substrate::binding::IncomingClassification {
label: 40,
instance: 0,
has_fin: false,
channel: hibana::substrate::binding::Channel::new(7),
})
}
fn on_recv<'a>(
&'a mut self,
_channel: hibana::substrate::binding::Channel,
scratch: &'a mut [u8],
) -> Result<
hibana::substrate::wire::Payload<'a>,
hibana::substrate::binding::TransportOpsError,
> {
scratch[..4].copy_from_slice(&[1, 2, 3, 4]);
Ok(hibana::substrate::wire::Payload::new(&scratch[..4]))
}
fn policy_signals_provider(
&self,
) -> Option<&dyn hibana::substrate::policy::PolicySignalsProvider> {
Some(self)
}
}Binding rules:
poll_incoming_for_lane()is lane-local demux onlyon_recv()reads from the already selected channel and returns a borrowed payload viewpolicy_signals_provider()is the only public input source for slot-scoped signals
Supporting binding owners:
Channel,ChannelDirection, andChannelKeyidentify stream and channel endpointsChannelStoreis the storage contract when the binding owns multiple channelsTransportOpsErroris the canonical binding-side I/O error
Dynamic policy stays explicit:
- annotate the choreography with
Program::policy::<POLICY_ID>() - register a resolver with
set_resolver::<POLICY_ID, ROLE, _>(...) - read inputs through
ResolverContext::input(index)and attrs throughResolverContext::attr(id) - return
Result<DynamicResolution, ResolverError>
ResolverContext is intentionally small: input(index) and attr(id) are the
only public accessors.
The public policy-owner surface is intentionally narrow:
hibana::substrate::policy::PolicySlotowns the generic slot identity- an external policy engine may execute inside the same resolver slot, but that
engine is not part of the
hibanacrate surface - policy execution is fail-closed; verifier, trap, and fuel failures reject rather than falling through
Useful policy owners and helpers:
ContextIdandContextValuefor fixed-width policy inputs and attrsPolicyAttrsfor the attribute bag copied into resolver contextPolicySignalsfor slot-scoped inputs delivered byPolicySignalsProviderResolverRef::from_state()for borrowed-state resolversResolverRef::from_fn()for stateless callbackshibana::substrate::policy::core::*for fixed metadata such asRV_ID,SESSION_ID,LANE,QUEUE_DEPTH,SRTT_US,PTO_COUNT,IN_FLIGHT_BYTES, andTRANSPORT_ALGORITHM
Example resolver:
const POLICY_ID: u16 = 7;
struct RoutePolicy {
preferred_arm: u8,
}
fn route_resolver(
policy: &RoutePolicy,
ctx: hibana::substrate::policy::ResolverContext,
) -> Result<hibana::substrate::policy::DynamicResolution, hibana::substrate::policy::ResolverError>
{
if ctx.input(0) != 0 {
return Ok(hibana::substrate::policy::DynamicResolution::RouteArm {
arm: policy.preferred_arm,
});
}
if ctx
.attr(hibana::substrate::policy::core::QUEUE_DEPTH)
.is_some_and(|value| value.as_u32() > 128)
{
return Err(hibana::substrate::policy::ResolverError::Reject);
}
Ok(hibana::substrate::policy::DynamicResolution::Defer { retry_hint: 1 })
}
let route_policy = RoutePolicy { preferred_arm: 1 };
cluster.set_resolver::<POLICY_ID, 0, _>(
rv_id,
&CLIENT,
hibana::substrate::policy::ResolverRef::from_state(&route_policy, route_resolver),
)?;hibana::substrate::wire::{Payload, WireEncode, WirePayload} is the canonical
payload seam.
hibana::substrate::transport::{TransportEvent, TransportEventKind, TransportSnapshot} is the canonical transport-observation seam.
If a payload type crosses the wire and is not already a codec type, implement
WireEncode plus WirePayload. Borrowed payload views use
type Decoded<'a> = ...; fixed-width by-value payloads stay on the same
contract with type Decoded<'a> = Self.
Transport telemetry is surfaced two ways:
- resolvers read snapshot data through
ResolverContext::attr()andhibana::substrate::policy::core::* - transports emit semantic events through
TransportEventandTransportEventKind - codec failures report through
CodecError - transport failures report through
TransportError TransportMetricsturns implementation-specific counters intoTransportSnapshot
Example snapshot construction:
let snapshot = hibana::substrate::transport::TransportSnapshot::new(Some(500), Some(2))
.with_retransmissions(Some(1))
.with_congestion_window(Some(65_536))
.with_in_flight(Some(4096))
.with_algorithm(Some(hibana::substrate::transport::TransportAlgorithm::Cubic));
let transport_event = hibana::substrate::transport::TransportEvent::new(
hibana::substrate::transport::TransportEventKind::Ack,
42,
1200,
0,
);
let _ = (snapshot.queue_depth, transport_event.packet_number);TransportSnapshot uses builder-style enrichment:
TransportSnapshot::new(latency_us, queue_depth)with_congestion_marks,with_pacing_interval,with_retransmissionswith_pto_count,with_srtt,with_latest_ackwith_congestion_window,with_in_flight,with_algorithm
hibana does not define a built-in management protocol.
If an integration needs policy distribution, tap streaming, or any other
cluster-management session, that choreography lives outside the hibana crate.
hibana only provides the neutral seams needed to compose, project, attach,
and drive that choreography.
Management-style usage still follows the normal substrate path:
use hibana::g;
use hibana::g::advanced::project;
const PROGRAM: g::Program<_> =
g::seq(MGMT_PREFIX, APP);
let controller_program = project(&PROGRAM);
let cluster_program = project(&PROGRAM);
let controller = cluster.enter(
rv_id,
sid,
&controller_program,
hibana::substrate::binding::NoBinding,
)?;
let cluster_role = cluster.enter(
rv_id,
sid,
&cluster_program,
hibana::substrate::binding::NoBinding,
)?;
let _ = (controller, cluster_role);The canonical local validation flow is:
bash ./.github/scripts/check_policy_surface_hygiene.sh
bash ./.github/scripts/check_surface_hygiene.sh
bash ./.github/scripts/check_boundary_contracts.sh
bash ./.github/scripts/check_plane_boundaries.sh
bash ./.github/scripts/check_mgmt_boundary.sh
bash ./.github/scripts/check_resolver_context_surface.sh
bash ./.github/scripts/check_warning_free.sh
bash ./.github/scripts/check_direct_projection_binary.sh
bash ./.github/scripts/check_no_std_build.sh
cargo check --all-targets -p hibana
cargo check --no-default-features --lib -p hibana
cargo test -p hibana --features std
cargo test -p hibana --test ui --features std
cargo test -p hibana --test policy_replay --features std
cargo test -p hibana --test public_surface_guards --features std
cargo test -p hibana --test substrate_surface --features std
cargo test -p hibana --test docs_surface --features stdThese checks keep the public surface small, keep no_std healthy, and guard
the compile-time guarantees described above.
Before pushing, also verify these invariants:
hibana/src/**/*.rsstays protocol-neutral- route authority stays
Ack | Resolver | Poll - static unprojectable route stays compile-error, not runtime rescue
- typed projection stays intact
- substrate names do not leak back into the app surface