ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
signaling.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file signaling.h
6 * @brief Application layer signaling: the bucket the server's state is read from, and the one way a
7 * connection the application layer owns is killed.
8 *
9 * Signaling in the control-plane sense - what the server knows about itself, kept apart from the data
10 * it is moving.
11 *
12 * **Signaling owns no state, and it never gathers any.** It originates nothing. The active function
13 * in the server loop already knows each fact at the instant it becomes true, so it deposits it here
14 * then, and only when there is something to deposit. ::SignalingNs::know hands the bucket back; it does
15 * not compose, poll, or ask an owner for anything.
16 *
17 * Both halves of that matter. A bucket that gathered on read would recompute what the loop had
18 * already established, and would interleave reads of owners that are moving underneath it. A bucket
19 * that owned its fields would be a second tally beside the real one, drifting the first time an owner
20 * changed without telling it. Depositing at the point of truth is neither: the fact is written once,
21 * by the code that had it.
22 *
23 * The problem it solves is that the state had no single place to be read from. protocore_stats(),
24 * protocore_metrics(), and protocore_diag() each walk a different set of owners and assemble their own picture, so
25 * the same question already has three answers, and a fourth reader would write a fourth.
26 *
27 * **Kill, for applications that do not talk transport.** An application at this layer has no
28 * transport dependency and should not acquire one just to hang up: reaching for Tcp.conn->close() would
29 * put an L4 include in L7 code and make every such application know about slots and PCBs. This is the
30 * seam instead. The decision is the application's and the teardown is the transport's.
31 *
32 * It also lets a remote kill its own connection - and only its own, because a remote request arrives
33 * on its slot and never supplies a slot number, so it has no way to name another. That is structural
34 * rather than a check, which is why there is no permission test here and no way to forge past one.
35 *
36 * A module that already speaks transport keeps calling transport directly. This does not replace
37 * Tcp.conn->close(); it means an application never has to reach for it.
38 *
39 * This is not SSH signaling. RFC 4254 signaling delivers a POSIX signal to a remote process over a
40 * channel and is implemented in ssh_flow_control; the two share a word and nothing else.
41 *
42 * Single-accessor like the rest of the server: use it from the worker that owns the slot.
43 *
44 * @author Douglas Quigg (dstroy0)
45 * @date 2026
46 */
47
48#ifndef PROTOCORE_SIGNALING_H
49#define PROTOCORE_SIGNALING_H
50
51#include "protocore_config.h"
52
54
55/**
56 * @brief The server's state, as the loop deposited it.
57 *
58 * A struct rather than a set of getters so a reader takes one consistent picture in one call instead
59 * of interleaving reads while the loop runs between them.
60 */
61typedef struct
62{
63 uint32_t uptime_ms; ///< Milliseconds the server has been up.
64 uint32_t requests_total; ///< Responses sent.
65 uint32_t responses_2xx;
66 uint32_t responses_4xx;
67 uint32_t responses_5xx;
68
69 // Masks, not counts. Which slot and which listener is the fact the pools already hold, and a
70 // count throws it away: __builtin_popcount recovers the tally from the mask in one instruction,
71 // while nothing recovers the identity from a tally. It is the shape the pools are allocated with
72 // (protocore_conn_alloc_free in tcp.c, the SFTP handle table), so a reader comparing the bucket against
73 // the pool is comparing like with like.
74 uint32_t conns_active; ///< One bit per connection slot in use.
75 uint32_t listeners_up; ///< One bit per bound listener.
77
78static_assert(CONN_POOL_SLOTS <= 32, "protocore_signal_snapshot::conns_active is one 32-bit word, one bit per slot");
79static_assert(MAX_LISTENERS <= 32, "protocore_signal_snapshot::listeners_up is one 32-bit word, one bit per listener");
80
81/** @brief What one deposit carries: a response's status, or the loop's own figures. */
82typedef struct
83{
84 int code; ///< the status that was sent, which is what selects the class tally
85 uint32_t uptime_ms; ///< how long the server has been up
86 uint32_t conns_active; ///< one bit per occupied connection slot
87 uint32_t listeners_up; ///< one bit per listener that is bound
89
90/**
91 * @brief The server's signalling bucket.
92 *
93 * A caller sets the members a call takes, invokes it through ::Signal, and reads the outcome off the
94 * same handle. The bucket itself is behind @ref internal.
95 *
96 * @var SignalingNs::put what one deposit carries
97 * @var SignalingNs::slot the connection a kill names
98 * @var SignalingNs::out where a read copies the bucket
99 * @var SignalingNs::know copy the bucket out; no gathering, this is what the loop deposited
100 * @var SignalingNs::reset empty the bucket
101 * @var SignalingNs::put_response deposit a response, from the send path, as the status goes out
102 * @var SignalingNs::put_tick deposit what the loop iteration already established
103 * @var SignalingNs::kill end a connection, for an application that does not talk transport
104 *
105 * know copies rather than handing out a pointer: a reader formats several fields and the loop
106 * deposits between its reads, so lending the storage would let one report mix two server states.
107 *
108 * put_tick is one call rather than three because these arrive together: the loop reads the clock
109 * every iteration for the idle-timeout sweep and walks the slots to service them, and the listener
110 * pool is touched constantly. Every value is in hand at the moment of the call, so the deposit costs
111 * the stores and nothing else.
112 *
113 * kill is a plain forward: no liveness test, no result. Transport owns the slot's lifetime and its
114 * idle sweep reaps a stale one regardless, so a check here would answer a question transport has
115 * already answered, and the answer could be stale before the caller read it.
116 */
123
124/** @brief The operands and the outcome. */
125extern SignalVars SignalV;
126
127/** @brief The entries. */
128typedef struct
129{
130 void (*const know)(uint8_t *work);
131 void (*const reset)(uint8_t *work);
132 void (*const put_response)(uint8_t *work);
133 void (*const put_tick)(uint8_t *work);
134 void (*const kill)(uint8_t *work);
136
137// What the table binds, defined once in the .c and taking one parameter each: everything
138// else an entry needs is an operand in SignalV or a region of the borrow at a fixed offset.
139void protocore_signal_know(uint8_t *work);
140void protocore_signal_reset(uint8_t *work);
142void protocore_signal_put_tick(uint8_t *work);
143void protocore_signal_kill(uint8_t *work);
144
145// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
146// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
147// `Signal.know(work)` resolves to a named function and becomes a DIRECT call. An extern table
148// leaves the call indirect and the symbol live at every level, -O2 -flto included.
149static const SignalingNs Signal __attribute__((unused)) = {
151 .reset = protocore_signal_reset,
152 .put_response = protocore_signal_put_response,
153 .put_tick = protocore_signal_put_tick,
154 .kill = protocore_signal_kill,
155};
156
157/**
158 * @brief The PROTOCORE_SIGNALING_BORROW bytes this module's state lives in.
159 *
160 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
161 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
162 * walks, so the state lasts the life of the program.
163 *
164 * @return the span.
165 */
167
169
170#endif // PROTOCORE_SIGNALING_H
#define CONN_POOL_SLOTS
#define MAX_LISTENERS
Maximum number of simultaneously active listener ports.
void protocore_signal_reset(uint8_t *work)
void protocore_signal_put_tick(uint8_t *work)
uint8_t * protocore_signaling_span(void)
The PROTOCORE_SIGNALING_BORROW bytes this module's state lives in.
void protocore_signal_know(uint8_t *work)
void protocore_signal_put_response(uint8_t *work)
void protocore_signal_kill(uint8_t *work)
SignalVars SignalV
The operands and the outcome.
What one deposit carries: a response's status, or the loop's own figures.
Definition signaling.h:83
uint32_t listeners_up
one bit per listener that is bound
Definition signaling.h:87
uint32_t uptime_ms
how long the server has been up
Definition signaling.h:85
uint32_t conns_active
one bit per occupied connection slot
Definition signaling.h:86
int code
the status that was sent, which is what selects the class tally
Definition signaling.h:84
SignalPutArgs put
Definition signaling.h:119
uint8_t slot
Definition signaling.h:120
protocore_signal_snapshot * out
Definition signaling.h:121
The entries.
Definition signaling.h:129
void(*const know)(uint8_t *work)
Definition signaling.h:130
The server's state, as the loop deposited it.
Definition signaling.h:62
uint32_t conns_active
One bit per connection slot in use.
Definition signaling.h:74
uint32_t uptime_ms
Milliseconds the server has been up.
Definition signaling.h:63
uint32_t requests_total
Responses sent.
Definition signaling.h:64
uint32_t listeners_up
One bit per bound listener.
Definition signaling.h:75
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
#define PROTOCORE_END_DECLS
Definition types.h:97