Split generated leaf runtime modules

This commit is contained in:
Michael Mikovsky
2026-06-01 13:35:30 -06:00
parent 693ba7c040
commit b4344a8d6a
5 changed files with 233 additions and 202 deletions
@@ -1,197 +1,16 @@
use alloc::collections::VecDeque;
#[cfg(feature = "interface")]
use crate::interface::{InterfaceEventKind, InterfaceStore, InterfaceTarget};
use crate::protocol::{
Endpoint, Packet, PacketQueue, Procedure, ProcedureOut, Session, SessionEntry, SessionFamily,
use crate::{
interface::{InterfaceEventKind, InterfaceStore, InterfaceTarget},
protocol::{
Endpoint, Packet, Procedure, ProcedureOut, Session, SessionEntry, SessionFamily,
SessionInitError, SessionStatus,
},
};
/// Retry queue shared by generated leaves.
///
/// Leaf-level retry queue shared by generated leaves.
///
/// Sessions route directly through `Endpoint` to keep their runtime shape small. This
/// queue remains only for one-shot procedures, whose handlers still use `ProcedureOut`
/// and should not route while the procedure is borrowing leaf state.
pub struct LeafOutbox {
packets: VecDeque<LeafOutboxEntry>,
}
/// One packet retained by a leaf-level retry queue.
///
/// Procedure responses from different generated branches share one queue. Storing the
/// owner beside the packet keeps route logging precise without exposing another public
/// queue type.
#[derive(Clone)]
struct LeafOutboxEntry {
packet: Packet,
#[cfg(feature = "interface")]
target: Option<InterfaceTarget>,
}
impl LeafOutbox {
/// Creates an empty leaf-level outbox.
pub fn new() -> Self {
Self {
packets: VecDeque::new(),
}
}
/// Adds one packet to the retry queue.
pub fn push(&mut self, packet: Packet) {
self.packets.push_back(LeafOutboxEntry {
packet,
#[cfg(feature = "interface")]
target: None,
});
}
/// Adds all packets from `packets` in FIFO order.
pub fn extend(&mut self, packets: PacketQueue) {
for packet in packets {
self.push(packet);
}
}
/// Returns the number of queued packets.
pub fn len(&self) -> usize {
self.packets.len()
}
/// Returns true when the queue has no pending packets.
pub fn is_empty(&self) -> bool {
self.packets.is_empty()
}
/// Adds one packet with a runtime-known interface target.
#[cfg(feature = "interface")]
pub(crate) fn push_for_target(&mut self, packet: Packet, target: InterfaceTarget) {
self.packets.push_back(LeafOutboxEntry {
packet,
target: Some(target),
});
}
/// Adds all packets with the same runtime-known interface target.
#[cfg(feature = "interface")]
pub(crate) fn extend_for_target(&mut self, packets: PacketQueue, target: InterfaceTarget) {
for packet in packets {
self.push_for_target(packet, target);
}
}
}
impl Default for LeafOutbox {
fn default() -> Self {
Self::new()
}
}
/// Dispatches one packet into a generated session family.
///
/// The macro picks `S` and the family field. This helper owns the boring details:
/// find the hook, initialize missing sessions, and route rejected responses. The
/// interface build uses the sibling logging helper so the smallest endpoint binary
/// does not mention the interface logging types on its hot update path.
pub fn dispatch_session<L, S>(
endpoint: &mut Endpoint,
leaf: &mut L,
family: &mut SessionFamily<S>,
packet: Packet,
) where
S: Session<L>,
{
let hook_id = packet.hook_id;
let procedure_id = S::PROCEDURE_ID;
if let Some(entry) = family
.entries
.iter_mut()
.find(|entry| entry.hook_id == hook_id)
{
entry.inbox.push_back(packet);
return;
}
let Ok(path) = endpoint.hook_path(hook_id) else {
return;
};
match S::init(leaf, packet) {
Ok(state) => {
family.entries.push(SessionEntry::new(hook_id, state));
}
Err(SessionInitError::Rejected) => {}
Err(SessionInitError::Response { data, end_hook }) => {
let packet = Packet {
hook_id,
end_hook,
path,
procedure_id,
data,
};
let _ = endpoint.add_outbound(packet);
}
}
}
/// Updates every live session in one generated session family.
pub fn update_session_family<L, S>(
endpoint: &mut Endpoint,
leaf: &mut L,
family: &mut SessionFamily<S>,
) where
S: Session<L>,
{
for entry in &mut family.entries {
if entry.closed {
continue;
}
let status = S::update(leaf, &mut entry.state, &mut entry.inbox, endpoint);
if matches!(status, SessionStatus::Closed) {
entry.closed = true;
}
}
family.entries.retain(|entry| !entry.closed);
}
/// Dispatches one packet into a generated one-shot procedure.
pub fn dispatch_procedure<L, P>(
leaf: &mut L,
endpoint: &mut Endpoint,
packet: Packet,
outbox: &mut LeafOutbox,
) where
P: Procedure<L>,
{
let hook_id = packet.hook_id;
let mut procedure_out =
ProcedureOut::new(hook_id, parent_reply_path(endpoint), P::PROCEDURE_ID);
P::handle(leaf, endpoint, packet, &mut procedure_out);
let packets = procedure_out.into_packets();
outbox.extend(packets);
}
/// Flushes a generated leaf-level outbox through endpoint routing.
pub fn flush_leaf_outbox(endpoint: &mut Endpoint, outbox: &mut LeafOutbox) -> bool {
while let Some(entry) = outbox.packets.front() {
if endpoint.add_outbound(entry.packet.clone()).is_err() {
return false;
}
outbox.packets.pop_front();
}
true
}
use super::{LeafOutbox, procedure::parent_reply_path};
/// Dispatches one packet into a generated session family with interface logging.
#[cfg(feature = "interface")]
pub fn dispatch_session_interface<L, S>(
endpoint: &mut Endpoint,
leaf_id: u32,
@@ -295,7 +114,6 @@ pub fn dispatch_session_interface<L, S>(
}
/// Updates every live session in one generated session family with interface logging.
#[cfg(feature = "interface")]
pub fn update_session_family_interface<L, S>(
endpoint: &mut Endpoint,
leaf_id: u32,
@@ -334,7 +152,6 @@ pub fn update_session_family_interface<L, S>(
}
/// Dispatches one packet into a generated one-shot procedure with interface logging.
#[cfg(feature = "interface")]
pub fn dispatch_procedure_interface<L, P>(
leaf_id: u32,
leaf: &mut L,
@@ -386,7 +203,6 @@ pub fn dispatch_procedure_interface<L, P>(
}
/// Flushes a generated leaf-level outbox through endpoint routing with interface logging.
#[cfg(feature = "interface")]
pub fn flush_leaf_outbox_interface(
endpoint: &mut Endpoint,
leaf_id: u32,
@@ -402,7 +218,6 @@ pub fn flush_leaf_outbox_interface(
})
}
#[cfg(feature = "interface")]
fn flush_outbox<T>(
endpoint: &mut Endpoint,
outbox: &mut VecDeque<T>,
@@ -422,7 +237,6 @@ fn flush_outbox<T>(
true
}
#[cfg(feature = "interface")]
fn flush_packet_with_target(
endpoint: &mut Endpoint,
target: InterfaceTarget,
@@ -460,12 +274,3 @@ fn flush_packet_with_target(
}
}
}
/// Returns the path used by generated procedure responses.
fn parent_reply_path(endpoint: &Endpoint) -> alloc::vec::Vec<u32> {
if endpoint.path.len() > 1 {
endpoint.path[..endpoint.path.len() - 1].to_vec()
} else {
endpoint.path.clone()
}
}
+23
View File
@@ -0,0 +1,23 @@
//! Runtime helpers used by generated leaves.
//!
//! The `unshell_leaf!` macro emits static dispatch code and delegates the reusable
//! session, procedure, retry, and interface-observation behavior to this module.
//! Keeping those pieces in normal Rust makes the macro easier to audit and keeps the
//! smallest endpoint builds free of interface-only logging paths.
mod outbox;
mod procedure;
mod session;
#[cfg(feature = "interface")]
mod interface;
pub use outbox::LeafOutbox;
pub use procedure::{dispatch_procedure, flush_leaf_outbox};
pub use session::{dispatch_session, update_session_family};
#[cfg(feature = "interface")]
pub use interface::{
dispatch_procedure_interface, dispatch_session_interface, flush_leaf_outbox_interface,
update_session_family_interface,
};
+84
View File
@@ -0,0 +1,84 @@
use alloc::collections::VecDeque;
#[cfg(feature = "interface")]
use crate::interface::InterfaceTarget;
use crate::protocol::{Packet, PacketQueue};
/// Retry queue shared by generated leaves.
///
/// Sessions route directly through `Endpoint` to keep their runtime shape small. This
/// queue remains only for one-shot procedures, whose handlers still use `ProcedureOut`
/// and should not route while the procedure is borrowing leaf state.
pub struct LeafOutbox {
pub(super) packets: VecDeque<LeafOutboxEntry>,
}
/// One packet retained by a leaf-level retry queue.
///
/// Procedure responses from different generated branches share one queue. Storing the
/// owner beside the packet keeps route logging precise without exposing another public
/// queue type.
#[derive(Clone)]
pub(super) struct LeafOutboxEntry {
pub(super) packet: Packet,
#[cfg(feature = "interface")]
pub(super) target: Option<InterfaceTarget>,
}
impl LeafOutbox {
/// Creates an empty leaf-level outbox.
pub fn new() -> Self {
Self {
packets: VecDeque::new(),
}
}
/// Adds one packet to the retry queue.
pub fn push(&mut self, packet: Packet) {
self.packets.push_back(LeafOutboxEntry {
packet,
#[cfg(feature = "interface")]
target: None,
});
}
/// Adds all packets from `packets` in FIFO order.
pub fn extend(&mut self, packets: PacketQueue) {
for packet in packets {
self.push(packet);
}
}
/// Returns the number of queued packets.
pub fn len(&self) -> usize {
self.packets.len()
}
/// Returns true when the queue has no pending packets.
pub fn is_empty(&self) -> bool {
self.packets.is_empty()
}
/// Adds one packet with a runtime-known interface target.
#[cfg(feature = "interface")]
pub(crate) fn push_for_target(&mut self, packet: Packet, target: InterfaceTarget) {
self.packets.push_back(LeafOutboxEntry {
packet,
target: Some(target),
});
}
/// Adds all packets with the same runtime-known interface target.
#[cfg(feature = "interface")]
pub(crate) fn extend_for_target(&mut self, packets: PacketQueue, target: InterfaceTarget) {
for packet in packets {
self.push_for_target(packet, target);
}
}
}
impl Default for LeafOutbox {
fn default() -> Self {
Self::new()
}
}
+46
View File
@@ -0,0 +1,46 @@
use alloc::vec::Vec;
use crate::protocol::{Endpoint, Packet, Procedure, ProcedureOut};
use super::LeafOutbox;
/// Dispatches one packet into a generated one-shot procedure.
pub fn dispatch_procedure<L, P>(
leaf: &mut L,
endpoint: &mut Endpoint,
packet: Packet,
outbox: &mut LeafOutbox,
) where
P: Procedure<L>,
{
let hook_id = packet.hook_id;
let mut procedure_out =
ProcedureOut::new(hook_id, parent_reply_path(endpoint), P::PROCEDURE_ID);
P::handle(leaf, endpoint, packet, &mut procedure_out);
let packets = procedure_out.into_packets();
outbox.extend(packets);
}
/// Flushes a generated leaf-level outbox through endpoint routing.
pub fn flush_leaf_outbox(endpoint: &mut Endpoint, outbox: &mut LeafOutbox) -> bool {
while let Some(entry) = outbox.packets.front() {
if endpoint.add_outbound(entry.packet.clone()).is_err() {
return false;
}
outbox.packets.pop_front();
}
true
}
/// Returns the path used by generated procedure responses.
pub(super) fn parent_reply_path(endpoint: &Endpoint) -> Vec<u32> {
if endpoint.path.len() > 1 {
endpoint.path[..endpoint.path.len() - 1].to_vec()
} else {
endpoint.path.clone()
}
}
+73
View File
@@ -0,0 +1,73 @@
use crate::protocol::{
Endpoint, Packet, Session, SessionEntry, SessionFamily, SessionInitError, SessionStatus,
};
/// Dispatches one packet into a generated session family.
///
/// The macro picks `S` and the family field. This helper owns the boring details:
/// find the hook, initialize missing sessions, and route rejected responses. The
/// interface build uses the sibling logging helper so the smallest endpoint binary
/// does not mention the interface logging types on its hot update path.
pub fn dispatch_session<L, S>(
endpoint: &mut Endpoint,
leaf: &mut L,
family: &mut SessionFamily<S>,
packet: Packet,
) where
S: Session<L>,
{
let hook_id = packet.hook_id;
let procedure_id = S::PROCEDURE_ID;
if let Some(entry) = family
.entries
.iter_mut()
.find(|entry| entry.hook_id == hook_id)
{
entry.inbox.push_back(packet);
return;
}
let Ok(path) = endpoint.hook_path(hook_id) else {
return;
};
match S::init(leaf, packet) {
Ok(state) => {
family.entries.push(SessionEntry::new(hook_id, state));
}
Err(SessionInitError::Rejected) => {}
Err(SessionInitError::Response { data, end_hook }) => {
let packet = Packet {
hook_id,
end_hook,
path,
procedure_id,
data,
};
let _ = endpoint.add_outbound(packet);
}
}
}
/// Updates every live session in one generated session family.
pub fn update_session_family<L, S>(
endpoint: &mut Endpoint,
leaf: &mut L,
family: &mut SessionFamily<S>,
) where
S: Session<L>,
{
for entry in &mut family.entries {
if entry.closed {
continue;
}
let status = S::update(leaf, &mut entry.state, &mut entry.inbox, endpoint);
if matches!(status, SessionStatus::Closed) {
entry.closed = true;
}
}
family.entries.retain(|entry| !entry.closed);
}