OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb commits / 3654abb

skills: hrpc-c and hyperschema-c — the C codegen half of the stack
Both are JS BUILD-TIME tools that emit C. Node runs the generator; the output links libcompact (+ librpc for hrpc-c) and needs no JavaScript at runtime. That distinction is the whole reason they fit a pure-C/C++ daemon, and it is the first thing each skill says. Why they were missing and why they matter: our hyperswarm-peer hand-writes src/dht_messages.h and src/compact_encoding.h, and the swarm-daemon design was about to hand-roll a UDS protocol with a hand-written type/id header. hyperschema-c generates the first, libcompact IS the second, and hrpc-c generates the third — typed, from a schema, with five handler shapes including streams. Meanwhile upstream is moving the other way: hyperdht 6.34.0 pulled in hyperdht-address (hyperschema-backed), and libhc's own commits read "generate manifest encoding via hyperschema-c". Written against UPSTREAM (ccae70b / 02253b8, both 2026-08-29) via the API and by reading lib/codegen.js, not against the KB clones — hyperschema-c's clone is ~3 months stale (727039f, 2026-06-04) and hrpc-c's is 3 commits behind. Each skill says so, because reading a stale clone as current is what put a whole parity audit on hyperdht 6.29.0 while upstream was at 6.34.0. Recorded from source rather than paraphrased: the generated preencode/encode/decode triad (the same shape our hand-written codec already uses, so generated codecs drop into existing call sites), versioned types as tagged unions, the full 27-entry field type table, the five hrpc handler kinds, and the two memory rules that will bite — every *_encode_* mallocs and the CALLER frees, and decoded views point INTO the inbound frame buffer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3654abbadfae56e747021a52e258d118e6ec3aec
by plankatron <fleeky@gmail.com> · 2026-09-10T15:41:48.000Z

Changed files (2) +222 -0

.claude/skills/hrpc-c/SKILL.md +110 -0
……@@ -0,0 +1,110 @@
1---
2name: hrpc-c
3description: Generate typed, sans-io C RPC from an HRPC schema — encoders, decoders and a dispatcher over the bare-rpc wire, built on librpc + libcompact and reusing hyperschema-c structs. Read before hand-rolling any local-socket or daemon protocol in C. Keywords: hrpc-c, RPC, C, sans-io, librpc, libcompact, hyperschema-c, bare-rpc, dispatcher, unary, event, stream, duplex, daemon, UDS, codegen.
4---
5
6# hrpc-c
7
8**A JS BUILD-TIME TOOL THAT EMITS C.** Node runs the generator; the output links
9`librpc` + `libcompact` and needs **no JavaScript at runtime**. That is what makes
10it usable in a pure-C/C++ daemon.
11
12⚠ **Verified 2026-09-10 against UPSTREAM `02253b8` (2026-08-29)** — README via the
13GitHub API, generated shapes read from `lib/codegen.js` in the clone. **The KB
14clone `holepunchto/repos/tier-new/hrpc-c` is at `90e9902` (2026-08-10)**; the
15three commits since are CI plus *"Compare against the descriptor instead of
16re-deriving the payload rule"*. Re-check before trusting details not marked as
17read from upstream.
18
19## Sans-io is the point
20
21From the README: *"the generated code is pure functions: it never owns a socket,
22an event loop, or stream state. You call the typed encoders to produce frames,
23hand inbound frames to the dispatcher, and do the actual I/O and stream lifecycle
24yourself."*
25
26So it composes with whatever carries bytes — a Unix socket, a Protomux channel, a
27pipe. It does not impose a transport.
28
29## Why it matters to us
30
31The generic swarm daemon design (`blender-bridge/docs/P2P_BRIDGE_SPEC.md`, and
32the `swarmd` sketch) hand-rolled a UDS protocol: length-prefixed frames, a
33hand-written type/id header, hand-written compact-encoding. **hrpc-c generates
34exactly that**, typed, from a schema, with streaming shapes included — and
35`libcompact` already is the C codec we hand-wrote as
36`hyperswarm-peer/src/compact_encoding.h`.
37
38⚠ It speaks the **bare-rpc wire protocol**. That is a protocol, not a runtime
39dependency: a C program can speak it to another C program with no Bare anywhere.
40
41## Usage
42
43`CHRPC` extends the JS `hrpc` builder:
44
45```js
46const CHyperschema = require('hyperschema-c')
47const CHRPC = require('hrpc-c')
48
49const schema = CHyperschema.from('./spec')
50const greeter = schema.namespace('greeter')
51greeter.register({ name: 'hello-request', fields: [{ name: 'id', type: 'uint', required: true }] })
52greeter.register({ name: 'hello-response', fields: [{ name: 'greeting', type: 'uint', required: true }] })
53
54const hrpc = new CHRPC(schema, './spec')
55hrpc.namespace('greeter').register({
56 name: 'hello',
57 request: { name: '@greeter/hello-request', stream: false },
58 response: { name: '@greeter/hello-response', stream: false }
59})
60
61CHyperschema.toDisk(schema)
62CHRPC.toDisk(hrpc)
63```
64
65`toDisk` writes five files: `hrpc.json` (the schema), `hrpc.h` (shared,
66schema-independent runtime types and enums), `<target>.h` / `<target>.c`, and a
67`CMakeLists.txt`. `<target>` is the namespaces joined by `_` plus `_hrpc`.
68`toCode(opts)` returns `{ shared, header, source }` as strings instead.
69
70The generated `CMakeLists.txt` expects the consumer to provide `compact`, `rpc`
71and the schema target (default `<namespaces>_schema`, override with
72`opts.schemaTarget`).
73
74## The five handler kinds
75
76Chosen by the request/response shape. Command ids are the global, append-only ids
77from the schema; identifiers are namespaced `<ns>_...`.
78
79| kind | shape | you get |
80|---|---|---|
81| **unary** | request + response, neither streaming | `<ns>_encode_<name>`, `_error`, `_decode_<name>_response`, and `int (*<ns>_on_<name>)(void *ctx, const <req>_t *, <res>_t *, hrpc_error_t *)` |
82| **event** | `request.send: true`, no response | fire-and-forget encoder (id 0) + `void (*on)(void *ctx, const <req>_t *)` |
83| **response-stream** | `response.stream: true` | request encoder, server `_open`/`_chunk`/`_end`/`_error`, client `_stream_open` echo, `_decode_<name>_chunk`, `int (*on)(ctx, req, uint64_t stream_id)` |
84| **request-stream** | `request.stream: true` | client `_open`/`_chunk`/`_end`, server echo + chunk decode, one unary-style reply, `int (*on)(ctx, uint64_t stream_id)` |
85| **duplex** | both streaming | the union of both, fully `request_`/`response_` qualified |
86
87## ⛔ Memory rules — the two that will bite
88
89Both from the README, and both are the opposite of what a careless reader assumes:
90
91- **Every `*_encode_*` heap-allocates `*out`. THE CALLER FREES IT.** The generated
92 code mallocs the frame (`codegen.js:374-384`) and hands over ownership.
93- **Decoded views point INTO the inbound frame buffer.** `hrpc_error_t` strings
94 and chunk payloads are borrowed, not copied — anything kept past the read loop
95 must be copied out first, or it dangles the moment the buffer is reused.
96
97## Dependencies
98
99- [[libcompact]] — the C compact-encoding runtime (ABI-identical to the JS codec)
100- `librpc` — the C RPC codec, "low-level RPC codec implemented in C for wide
101 language support"
102- [[hyperschema-c]] — supplies the struct codecs the RPC layer carries
103
104## Related
105
106- [[hyperschema-c]] — generate the structs first; hrpc-c layers on top
107- [[hrpc]] — the JS builder `CHRPC` extends
108- `bare-linux` — a C host embedding Bare and talking to it over this wire. ⚠ That
109 route is a SHIM, not a library: the P2P half is JavaScript on a Bare worklet.
110 hrpc-c itself needs no JS at runtime; bare-kit does.
.claude/skills/hyperschema-c/SKILL.md +112 -0
……@@ -0,0 +1,112 @@
1---
2name: hyperschema-c
3description: Generate C struct codecs from a Hyperschema — the JS tool that emits preencode/encode/decode C on top of libcompact, so a C program speaks the same wire as the JS stack without hand-writing message codecs. Read before hand-porting any compact-encoded message to C. Keywords: hyperschema-c, codegen, C, libcompact, compact_state_t, preencode, encode, decode, schema, wire, dht_messages, versioned, tagged union.
4---
5
6# hyperschema-c
7
8**A JS BUILD-TIME TOOL THAT EMITS C.** Node runs the generator; the output is plain
9C that links `libcompact` and needs no JavaScript at runtime. That distinction is
10the whole reason it fits a pure-C/C++ daemon.
11
12⚠ **Verified 2026-09-10 against UPSTREAM `ccae70b` (2026-08-29)** via the GitHub
13API and by reading `lib/codegen.js` in the local clone. **The KB clone
14`holepunchto/repos/tier-new/hyperschema-c` is at `727039f` (2026-06-04) — about
15three months stale.** Re-check before trusting anything here that is not marked
16as read from upstream. (Reading a stale clone as current is what put a whole
17parity audit on hyperdht 6.29.0 while upstream was at 6.34.0 — see
18[[hrpc-c]] and hyperswarm-peer `docs/PARITY-hyperswarm.md` §2.)
19
20## Why it matters to us
21
22Our `hyperswarm-peer/src/dht_messages.h` is a **hand-written** codec for the
23HyperDHT wire. Upstream is moving the other way: hyperdht 6.34.0 pulled in
24`hyperdht-address`, which uses hyperschema, and `libhc`'s own commits read
25*"generate manifest encoding via hyperschema-c"*. So their encodings are becoming
26a GENERATED artifact from a shared schema while ours are typed by hand.
27
28Generating ours from the same schemas would make tracking upstream mechanical
29instead of an audit. Not done — recorded as an open question in
30`PARITY-hyperswarm.md` §6.3.
31
32## Usage
33
34```js
35const CHyperschema = require('hyperschema-c')
36
37const schema = CHyperschema.from('./spec')
38const { header, source } = schema.toCode() // strings
39CHyperschema.toDisk(schema, './output') // or write them
40```
41
42The whole public API, from `index.d.ts`:
43
44```ts
45declare class CHyperschema {
46 toCode(): { header: string; source: string }
47 static from(dir: string): CHyperschema
48 static toDisk(hyperschema: CHyperschema, dir?: string | null): void
49}
50```
51
52`toDisk` writes `<target>.h`, `<target>.c`, `schema.json` and a `CMakeLists.txt`.
53`<target>` comes from the namespaces: one namespace `hc` gives `hc_schema.{h,c}`;
54several give `<ns1>_<ns2>_schema`.
55
56## The generated shape
57
58Read from `lib/codegen.js:219-338`. Every type becomes a struct plus the same
59three functions:
60
61```c
62typedef struct <name>_s { /* fields */ } <name>_t;
63
64int <name>_preencode (compact_state_t *state, const <name>_t *value);
65int <name>_encode (compact_state_t *state, const <name>_t *value);
66int <name>_decode (compact_state_t *state, <name>_t *result);
67```
68
69⚠ **That is the same preencode → allocate → encode triad our hand-written
70`compact_encoding.h` already uses**, so generated codecs drop into the existing
71call sites without restructuring anything.
72
73**Versioned types become tagged unions** (`codegen.js:219`):
74
75```c
76typedef struct <name>_s {
77 uint64_t version;
78 union { /* one member per version */ };
79} <name>_t;
80```
81
82preencode/encode take the version by value; decode reads into it.
83
84## Field types
85
86The complete table (`codegen.js:19-46`). A type not in it is a build-time error
87naming the supported set — it never silently degrades.
88
89| schema | C |
90|---|---|
91| `bool` | `bool` |
92| `uint` / `int` | `uint64_t` / `int64_t` (varint) |
93| `uint8/16/32/64`, `int8/16/32/64` | the matching fixed-width type |
94| `uint24/40/48/56`, `int24/40/48/56` | widened to 32/64-bit |
95| `fixed32` / `fixed64` | `uint8_t` array |
96| `buffer` | `uint8_t` array (`uint8array` on the wire) |
97| `string`, `json` | `utf8_string_view_t` (both encode as utf8) |
98| `float32` / `float64` | `float` / `double` |
99
100⚠ `json` and `string` are the SAME wire type. The schema distinguishes intent,
101not bytes.
102
103## Dependencies
104
105Generated code needs **`libcompact`** (`compact_state_t`) and nothing else. The
106generated `CMakeLists.txt` expects the consumer to provide a `compact` target.
107
108## Related
109
110- [[hrpc-c]] — layers typed RPC on top of these struct codecs
111- [[libcompact]] — the C runtime the output links; ABI-identical to the JS codec
112- [[hyperschema]] — the JS schema definitions these are generated from