OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer commits / 1818f6d

swarmd: the control plane, dispatched by generated code end to end
A UDS frame is one bare-rpc message: librpc decodes it, the GENERATED dispatcher routes it, the handler answers, and the generated encoder produces the reply. Not one byte is parsed by hand, which was the entire argument for not hand-rolling the protocol. client swarmd_encode_hello (generated) -> UDS frame -> rpc_decode_message (librpc) -> swarmd_hrpc_dispatch (generated) -> handler -> reply encoded (generated) -> swarmd_decode_hello_response (generated) Gate: test/test_control.cpp, 10/10, over a real socket with a real client. Two ownership rules are enforced where they matter, both from hrpc-c's README and both the opposite of the careless assumption: - every *_encode_* HEAP-ALLOCATES and the CALLER FREES. Both send paths free the buffer after handing it to the socket. - decoded views point INTO the inbound frame buffer, which is reused as soon as the handler returns. on_hello COPIES the identity and the name out, and the gate asserts that specifically rather than trusting it. build.sh now compiles libcompact, libutf and librpc from source alongside the daemon — three small static libs, so a cmake-fetch tree would cost more than it explains. Still gcc/g++ only, still no npm. ⏳ Not built: swarm attachment (the UDX thread), channels, content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1818f6d3a861da41b9e286e48a2a67265dbbcd4d
by plankatron <fleeky@gmail.com> · 2026-09-10T17:14:04.000Z

Changed files (6) +399 -2

swarmd/.gitignore +1 -0
……@@ -1,2 +1,3 @@
11node_modules/
22test/test_uds_backpressure
3test/test_control
swarmd/README.md +12 -2
……@@ -28,7 +28,17 @@ encodings the same way, with `schema/build.js` and the resulting
2828- **libutf** — string views (pulled in by libcompact)
2929- **librpc** — the C RPC codec
3030
31## Build
32
33 ./build.sh # gcc/g++ only. No npm.
34 ./test/test_uds_backpressure
35 ./test/test_control
36
3137## Status
3238
33Wire generated and compiling. **No daemon yet** — see `../docs/SWARMD-DESIGN.md`,
34§9 for the order and §8 for the gates.
39 schema/ generated wire, committed 3316 lines
40 uds_server local socket + per-client buffering 7/7 (D2, D3)
41 control generated RPC dispatch on the socket 10/10
42
43⏳ Not built: swarm attachment (the UDX thread), channels, content. See
44`../docs/SWARMD-DESIGN.md` §9 for the order and §8 for the remaining gates.
swarmd/build.sh +27 -0
……@@ -10,15 +10,42 @@ INC="-Isrc -Ischema -I$KB/tier5/libcompact/include -I$KB/tier4/libutf/include -I
1010OBJ=${OBJ:-/tmp/swarmd-obj}
1111mkdir -p "$OBJ"
1212
13# The holepunch C libraries. Sources compiled in directly rather than via CMake:
14# swarmd is one small daemon, and a cmake-fetch tree for three static libs costs
15# more than it explains.
16echo "Building libcompact / libutf / librpc..."
17CLIBS=""
18for c in "$KB"/tier5/libcompact/src/*.c; do
19 o="$OBJ/compact_$(basename "$c" .c).o"
20 gcc -std=c99 -O2 -I"$KB/tier5/libcompact/include" -I"$KB/tier4/libutf/include" -c -o "$o" "$c" 2>/dev/null
21 CLIBS="$CLIBS $o"
22done
23for c in "$KB"/tier4/libutf/src/*.c "$KB"/tier4/libutf/src/*/*.c; do
24 [ -f "$c" ] || continue
25 o="$OBJ/utf_$(echo "$c" | md5sum | cut -c1-8).o"
26 gcc -std=c99 -O2 -I"$KB/tier4/libutf/include" -c -o "$o" "$c" 2>/dev/null || continue
27 CLIBS="$CLIBS $o"
28done
29for c in "$KB"/tier4/librpc/src/*.c; do
30 o="$OBJ/rpc_$(basename "$c" .c).o"
31 gcc -std=c99 -O2 -I"$KB/tier4/librpc/include" -I"$KB/tier5/libcompact/include" -I"$KB/tier4/libutf/include" -c -o "$o" "$c" 2>/dev/null || continue
32 CLIBS="$CLIBS $o"
33done
34
1335echo "Building generated wire..."
1436gcc -std=c99 -O2 $INC -c -o "$OBJ/swarmd_schema.o" schema/swarmd_schema.c
1537gcc -std=c99 -O2 $INC -c -o "$OBJ/swarmd_hrpc.o" schema/swarmd_hrpc.c
1638
1739echo "Building daemon sources..."
1840g++ -std=c++17 -O2 $INC -c -o "$OBJ/uds_server.o" src/uds_server.cpp
41g++ -std=c++17 -O2 $INC -c -o "$OBJ/control.o" src/control.cpp 2>/dev/null
1942
2043echo "Building gates..."
2144g++ -std=c++17 -O2 $INC -o test/test_uds_backpressure \
2245 test/test_uds_backpressure.cpp "$OBJ/uds_server.o"
2346
47g++ -std=c++17 -O2 $INC -o test/test_control \
48 test/test_control.cpp "$OBJ/uds_server.o" "$OBJ/control.o" \
49 "$OBJ/swarmd_schema.o" "$OBJ/swarmd_hrpc.o" $CLIBS
50
2451echo " OK"
swarmd/src/control.cpp +121 -0
……@@ -0,0 +1,121 @@
1#include "control.h"
2
3#include <cstdio>
4#include <cstdlib>
5#include <cstring>
6
7extern "C" {
8#include <compact.h>
9#include <rpc.h>
10}
11
12namespace swarmd {
13
14// The dispatcher's handlers are plain C function pointers with a void* ctx, so
15// the instance travels through ctx rather than through a capture.
16struct Ctx {
17 Control *self;
18 Control::Session *session;
19 ControlBackend *backend;
20 uint64_t *hellos;
21};
22
23static int on_hello(void *vctx, const swarmd_hello_request_t *req,
24 swarmd_hello_response_t *res, hrpc_error_t *) {
25 Ctx *ctx = (Ctx *)vctx;
26 // ⛔ COPY OUT. req's views point into the inbound frame buffer, which is
27 // reused the moment this returns.
28 memcpy(ctx->session->app_identity, req->app_identity, 32);
29 ctx->session->name.assign((const char *)req->name.data, req->name.len);
30 ctx->session->hello = true;
31 (*ctx->hellos)++;
32
33 res->client_id = 0; // filled by the caller, which knows it
34 memset(res->daemon_pubkey, 0, 32); // no transport attached yet
35 return 0;
36}
37
38static int on_status_get(void *vctx, const swarmd_none_t *,
39 swarmd_status_t *res, hrpc_error_t *) {
40 Ctx *ctx = (Ctx *)vctx;
41 memset(res, 0, sizeof(*res));
42 if (ctx->backend->status) ctx->backend->status(res);
43 return 0;
44}
45
46static int on_swarm_join(void *vctx, const swarmd_swarm_join_t *req,
47 swarmd_ok_t *res, hrpc_error_t *) {
48 Ctx *ctx = (Ctx *)vctx;
49 res->ok = ctx->backend->swarm_join
50 ? ctx->backend->swarm_join(req->topic, req->server, req->client)
51 : false;
52 return 0;
53}
54
55static int on_swarm_leave(void *vctx, const swarmd_topic_ref_t *req,
56 swarmd_ok_t *res, hrpc_error_t *) {
57 Ctx *ctx = (Ctx *)vctx;
58 res->ok = ctx->backend->swarm_leave ? ctx->backend->swarm_leave(req->topic) : false;
59 return 0;
60}
61
62void Control::attach(UdsServer *uds, ControlBackend backend) {
63 _uds = uds;
64 _backend = std::move(backend);
65 _uds->on_connect = [this](uint64_t id) { _sessions[id] = Session(); };
66 _uds->on_disconnect = [this](uint64_t id) { _sessions.erase(id); };
67 _uds->on_frame = [this](uint64_t id, const uint8_t *d, size_t n) { _on_frame(id, d, n); };
68}
69
70const Control::Session *Control::session(uint64_t client_id) const {
71 auto it = _sessions.find(client_id);
72 return it == _sessions.end() ? nullptr : &it->second;
73}
74
75void Control::_on_frame(uint64_t client_id, const uint8_t *data, size_t len) {
76 auto sit = _sessions.find(client_id);
77 if (sit == _sessions.end()) return;
78
79 compact_state_t state = { 0, len, (uint8_t *)data };
80 rpc_message_t msg;
81 if (rpc_decode_message(&state, &msg) < 0) {
82 fprintf(stderr, "[swarmd] client %llu sent an undecodable message — dropping it\n",
83 (unsigned long long)client_id);
84 return;
85 }
86
87 Ctx ctx = { this, &sit->second, &_backend, &_hellos };
88 swarmd_hrpc_handlers_t h;
89 memset(&h, 0, sizeof(h));
90 h.ctx = &ctx;
91 h.on_hello = on_hello;
92 h.on_status_get = on_status_get;
93 h.on_swarm_join = on_swarm_join;
94 h.on_swarm_leave = on_swarm_leave;
95
96 uint8_t *reply = nullptr;
97 size_t reply_len = 0;
98 _current_client = client_id;
99 const int r = swarmd_hrpc_dispatch(&h, &msg, &reply, &reply_len);
100 _current_client = 0;
101
102 if (r == hrpc_dispatch_reply && reply != nullptr) {
103 _uds->send(client_id, reply, reply_len);
104 // ⛔ THE CALLER FREES. Every *_encode_* heap-allocates its output.
105 free(reply);
106 }
107}
108
109void Control::push_status(uint64_t client_id) {
110 swarmd_status_t st;
111 memset(&st, 0, sizeof(st));
112 if (_backend.status) _backend.status(&st);
113
114 uint8_t *out = nullptr;
115 size_t out_len = 0;
116 if (swarmd_encode_on_status(&st, &out, &out_len) < 0 || out == nullptr) return;
117 _uds->send(client_id, out, out_len);
118 free(out); // caller frees
119}
120
121} // namespace swarmd
swarmd/src/control.h +70 -0
……@@ -0,0 +1,70 @@
1#ifndef SWARMD_CONTROL_H
2#define SWARMD_CONTROL_H
3
4// The control plane: generated RPC on top of the local socket.
5//
6// A UDS frame is one bare-rpc message. librpc decodes it, the GENERATED
7// dispatcher routes it to a handler, and any reply goes back as another frame.
8// Nothing here parses a payload by hand — that is the entire point of
9// generating the wire (docs/SWARMD-DESIGN.md §3).
10//
11// ⛔ TWO OWNERSHIP RULES, both from hrpc-c's README and both the opposite of
12// the careless assumption:
13// 1. every *_encode_* HEAP-ALLOCATES its output and THE CALLER FREES IT
14// 2. decoded views point INTO the inbound frame buffer, so anything kept
15// past the handler must be copied out
16// They are enforced at the two places they matter, marked below.
17
18#include <cstdint>
19#include <functional>
20#include <map>
21#include <string>
22
23#include "uds_server.h"
24
25extern "C" {
26#include "swarmd_hrpc.h"
27#include "swarmd_schema.h"
28}
29
30namespace swarmd {
31
32// What the daemon can actually answer. Filled in by the daemon; kept behind an
33// interface so the control plane can be gated with no swarm at all.
34struct ControlBackend {
35 std::function<void(swarmd_status_t *out)> status;
36 std::function<bool(const uint8_t topic[32], bool server, bool client)> swarm_join;
37 std::function<bool(const uint8_t topic[32])> swarm_leave;
38};
39
40class Control {
41public:
42 void attach(UdsServer *uds, ControlBackend backend);
43
44 // Per-client session state. A client that has not said HELLO has no
45 // identity, and identity is what echo-suppression and membership need.
46 struct Session {
47 bool hello = false;
48 uint8_t app_identity[32] = { 0 };
49 std::string name;
50 };
51
52 const Session *session(uint64_t client_id) const;
53 uint64_t hellos() const { return _hellos; }
54
55 // Daemon -> client events. Encoded with the generated encoders.
56 void push_status(uint64_t client_id);
57
58private:
59 void _on_frame(uint64_t client_id, const uint8_t *data, size_t len);
60
61 UdsServer *_uds = nullptr;
62 ControlBackend _backend;
63 std::map<uint64_t, Session> _sessions;
64 uint64_t _current_client = 0; // the client being dispatched, for handlers
65 uint64_t _hellos = 0;
66};
67
68} // namespace swarmd
69
70#endif
swarmd/test/test_control.cpp +168 -0
……@@ -0,0 +1,168 @@
1// Gate: a real client, over the real socket, through librpc and the GENERATED
2// dispatcher. docs/SWARMD-DESIGN.md §5.
3//
4// This is the proof that the codegen bet pays off end to end:
5//
6// client encodes with swarmd_encode_hello (generated)
7// -> UDS frame (uint32 LE length + payload)
8// -> rpc_decode_message (librpc)
9// -> swarmd_hrpc_dispatch (generated)
10// -> handler
11// -> reply encoded (generated)
12// -> UDS frame back
13// -> swarmd_decode_hello_response (generated)
14//
15// Not one byte of that is parsed by hand, which was the whole argument for not
16// hand-rolling the protocol.
17
18#include <cstdio>
19#include <cstdlib>
20#include <cstring>
21#include <vector>
22
23#include <sys/socket.h>
24#include <sys/un.h>
25#include <unistd.h>
26
27#include "control.h"
28
29extern "C" {
30#include <compact.h>
31#include <rpc.h>
32}
33
34static int checks = 0, failures = 0;
35static void check(bool c, const char *what) {
36 checks++;
37 if (c) { printf(" PASS: %s\n", what); return; }
38 failures++;
39 printf(" FAIL: %s\n", what);
40}
41
42static int dial(const char *path) {
43 int fd = socket(AF_UNIX, SOCK_STREAM, 0);
44 struct sockaddr_un a;
45 memset(&a, 0, sizeof(a));
46 a.sun_family = AF_UNIX;
47 strncpy(a.sun_path, path, sizeof(a.sun_path) - 1);
48 if (connect(fd, (struct sockaddr *)&a, sizeof(a)) != 0) { close(fd); return -1; }
49 return fd;
50}
51
52static void write_frame(int fd, const uint8_t *d, size_t n) {
53 uint8_t hdr[4] = { (uint8_t)(n & 0xff), (uint8_t)((n >> 8) & 0xff),
54 (uint8_t)((n >> 16) & 0xff), (uint8_t)((n >> 24) & 0xff) };
55 (void)!write(fd, hdr, 4);
56 (void)!write(fd, d, n);
57}
58
59static bool read_frame(int fd, std::vector<uint8_t> &out) {
60 uint8_t hdr[4];
61 ssize_t r = read(fd, hdr, 4);
62 if (r != 4) return false;
63 const uint32_t n = (uint32_t)hdr[0] | ((uint32_t)hdr[1] << 8) |
64 ((uint32_t)hdr[2] << 16) | ((uint32_t)hdr[3] << 24);
65 out.resize(n);
66 size_t got = 0;
67 while (got < n) {
68 r = read(fd, out.data() + got, n - got);
69 if (r <= 0) return false;
70 got += (size_t)r;
71 }
72 return true;
73}
74
75int main() {
76 char path[128];
77 snprintf(path, sizeof(path), "/tmp/swarmd-control-%d.sock", (int)getpid());
78
79 swarmd::UdsServer uds;
80 if (!uds.listen(path)) { printf("listen failed\n"); return 1; }
81
82 bool joined = false;
83 uint8_t joined_topic[32] = { 0 };
84
85 swarmd::ControlBackend backend;
86 backend.status = [](swarmd_status_t *out) {
87 out->dht_state = 2; // up
88 out->bootstrapped = true;
89 out->peers_connected = 3;
90 out->local_clients = 1;
91 };
92 backend.swarm_join = [&](const uint8_t topic[32], bool, bool) {
93 joined = true;
94 memcpy(joined_topic, topic, 32);
95 return true;
96 };
97
98 swarmd::Control control;
99 control.attach(&uds, backend);
100
101 int fd = dial(path);
102 check(fd >= 0, "client attached");
103 uds.poll(50);
104
105 // ---- HELLO, encoded by the generated client encoder ----
106 swarmd_hello_request_t hello;
107 memset(&hello, 0, sizeof(hello));
108 const char *name = "gate";
109 hello.name.data = (utf8_t *)name;
110 hello.name.len = strlen(name);
111 memset(hello.app_identity, 0x7e, 32);
112 hello.version = 1;
113
114 uint8_t *req = nullptr;
115 size_t req_len = 0;
116 check(swarmd_encode_hello(1, &hello, &req, &req_len) >= 0 && req != nullptr,
117 "generated encoder produced a request");
118 write_frame(fd, req, req_len);
119 free(req); // caller frees
120
121 for (int i = 0; i < 20; i++) uds.poll(5);
122 check(control.hellos() == 1, "the dispatcher routed HELLO to its handler");
123
124 const swarmd::Control::Session *s = control.session(1);
125 check(s && s->hello, "the session records that HELLO happened");
126 uint8_t want[32];
127 memset(want, 0x7e, 32);
128 check(s && memcmp(s->app_identity, want, 32) == 0,
129 "the app identity was COPIED OUT of the frame buffer, not aliased");
130 check(s && s->name == "gate", "and so was the name");
131
132 // ---- the reply comes back, decoded by the generated decoder ----
133 std::vector<uint8_t> frame;
134 check(read_frame(fd, frame), "a reply frame came back");
135 if (!frame.empty()) {
136 compact_state_t st = { 0, frame.size(), frame.data() };
137 rpc_message_t msg;
138 check(rpc_decode_message(&st, &msg) >= 0, "librpc decoded the reply");
139 swarmd_hello_response_t res;
140 hrpc_error_t err;
141 memset(&res, 0, sizeof(res));
142 check(swarmd_decode_hello_response(&msg, &res, &err) >= 0,
143 "the generated decoder read the response");
144 }
145
146 // ---- a second command, to prove dispatch is by command id ----
147 swarmd_swarm_join_t sj;
148 memset(&sj, 0, sizeof(sj));
149 memset(sj.topic, 0x11, 32);
150 sj.server = true;
151 sj.client = true;
152 uint8_t *req2 = nullptr;
153 size_t req2_len = 0;
154 swarmd_encode_swarm_join(2, &sj, &req2, &req2_len);
155 write_frame(fd, req2, req2_len);
156 free(req2);
157 for (int i = 0; i < 20; i++) uds.poll(5);
158
159 uint8_t want_topic[32];
160 memset(want_topic, 0x11, 32);
161 check(joined && memcmp(joined_topic, want_topic, 32) == 0,
162 "SWARM_JOIN reached the backend with the right topic");
163
164 close(fd);
165 uds.close_all();
166 printf("\n%d/%d passed\n", checks - failures, checks);
167 return failures ? 1 : 0;
168}