| … | … | @@ -0,0 +1,110 @@ |
| 1 | --- |
| 2 | name: hrpc-c |
| 3 | description: 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 |
| 10 | it usable in a pure-C/C++ daemon. |
| 11 | |
| 12 | ⚠ **Verified 2026-09-10 against UPSTREAM `02253b8` (2026-08-29)** — README via the |
| 13 | GitHub API, generated shapes read from `lib/codegen.js` in the clone. **The KB |
| 14 | clone `holepunchto/repos/tier-new/hrpc-c` is at `90e9902` (2026-08-10)**; the |
| 15 | three commits since are CI plus *"Compare against the descriptor instead of |
| 16 | re-deriving the payload rule"*. Re-check before trusting details not marked as |
| 17 | read from upstream. |
| 18 | |
| 19 | ## Sans-io is the point |
| 20 | |
| 21 | From the README: *"the generated code is pure functions: it never owns a socket, |
| 22 | an event loop, or stream state. You call the typed encoders to produce frames, |
| 23 | hand inbound frames to the dispatcher, and do the actual I/O and stream lifecycle |
| 24 | yourself."* |
| 25 | |
| 26 | So it composes with whatever carries bytes — a Unix socket, a Protomux channel, a |
| 27 | pipe. It does not impose a transport. |
| 28 | |
| 29 | ## Why it matters to us |
| 30 | |
| 31 | The generic swarm daemon design (`blender-bridge/docs/P2P_BRIDGE_SPEC.md`, and |
| 32 | the `swarmd` sketch) hand-rolled a UDS protocol: length-prefixed frames, a |
| 33 | hand-written type/id header, hand-written compact-encoding. **hrpc-c generates |
| 34 | exactly 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 |
| 39 | dependency: 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 |
| 46 | const CHyperschema = require('hyperschema-c') |
| 47 | const CHRPC = require('hrpc-c') |
| 48 | |
| 49 | const schema = CHyperschema.from('./spec') |
| 50 | const greeter = schema.namespace('greeter') |
| 51 | greeter.register({ name: 'hello-request', fields: [{ name: 'id', type: 'uint', required: true }] }) |
| 52 | greeter.register({ name: 'hello-response', fields: [{ name: 'greeting', type: 'uint', required: true }] }) |
| 53 | |
| 54 | const hrpc = new CHRPC(schema, './spec') |
| 55 | hrpc.namespace('greeter').register({ |
| 56 | name: 'hello', |
| 57 | request: { name: '@greeter/hello-request', stream: false }, |
| 58 | response: { name: '@greeter/hello-response', stream: false } |
| 59 | }) |
| 60 | |
| 61 | CHyperschema.toDisk(schema) |
| 62 | CHRPC.toDisk(hrpc) |
| 63 | ``` |
| 64 | |
| 65 | `toDisk` writes five files: `hrpc.json` (the schema), `hrpc.h` (shared, |
| 66 | schema-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 | |
| 70 | The generated `CMakeLists.txt` expects the consumer to provide `compact`, `rpc` |
| 71 | and the schema target (default `<namespaces>_schema`, override with |
| 72 | `opts.schemaTarget`). |
| 73 | |
| 74 | ## The five handler kinds |
| 75 | |
| 76 | Chosen by the request/response shape. Command ids are the global, append-only ids |
| 77 | from 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 | |
| 89 | Both 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. |