castrum 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -630
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1653 -1817
- package/dist/index.js +6172 -4294
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +111 -17
- package/docs/BENCHMARKS.md +117 -49
- package/docs/CASE_STUDY.md +243 -0
- package/docs/ENVIRONMENT.md +24 -7
- package/docs/FFI_BUN_GUIDE.md +335 -0
- package/docs/GETTING_STARTED.md +18 -34
- package/docs/HOW_TO_ADD_AN_OP.md +180 -0
- package/docs/INGRESS-ROUTER.md +82 -0
- package/docs/INGRESS.md +11 -7
- package/docs/NATIVE-ROUTE.md +77 -0
- package/docs/REPO_MAP.md +103 -53
- package/docs/adr/0001-two-wire-formats.md +1 -1
- package/docs/adr/0003-bun-builtins-delegation.md +6 -5
- package/docs/ai/LOCAL_DEV.md +81 -0
- package/docs/ai/TREE.md +1070 -0
- package/docs/bun-builtins-decision-matrix.md +25 -18
- package/index.ts +9 -16
- package/package.json +41 -28
- package/src/ingress/body.ts +167 -27
- package/src/ingress/decode/result-base.ts +10 -2
- package/src/ingress/handlers.ts +116 -285
- package/src/ingress/headers/security.ts +41 -0
- package/src/ingress/health.ts +1 -0
- package/src/ingress/index.ts +45 -169
- package/src/ingress/native-route.ts +182 -0
- package/src/ingress/packing/gather-raw-headers.ts +110 -22
- package/src/ingress/packing/header-packing.ts +5 -5
- package/src/ingress/packing/input-packer.ts +17 -3
- package/src/ingress/packing/route-wire.ts +197 -0
- package/src/ingress/packing/select-headers.ts +20 -7
- package/src/ingress/path-matcher.ts +115 -0
- package/src/ingress/response/baked-response.ts +302 -0
- package/src/ingress/router.ts +277 -0
- package/src/ingress/routes/common.ts +50 -1
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +3 -13
- package/src/ingress/routes/index.ts +5 -4
- package/src/ingress/routes/json-write.ts +26 -47
- package/src/ingress/routes/native.ts +130 -0
- package/src/ingress/routes/options.ts +2 -12
- package/src/ingress/routes/read.ts +3 -13
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +7 -7
- package/src/ingress/server.ts +83 -112
- package/src/ingress/sync.ts +151 -0
- package/src/ingress/types.ts +44 -1
- package/src/integration/pipeline.ts +3 -0
- package/src/integration/streaming.ts +2 -3
- package/src/integration/websocket.ts +4 -3
- package/src/loader/batch.ts +4 -0
- package/src/loader/cost.ts +1 -0
- package/src/loader/index.ts +43 -228
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/build/codecs.ts +736 -0
- package/src/native/ffi/build/compress.ts +248 -0
- package/src/native/ffi/build/instances.ts +598 -0
- package/src/native/ffi/build/parse.ts +539 -0
- package/src/native/ffi/build/util.ts +140 -0
- package/src/native/ffi/build.ts +94 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +22 -0
- package/src/native/ffi/types.ts +499 -0
- package/src/native/ffi.ts +307 -1357
- package/src/native/loader.ts +52 -6
- package/src/native/types/instances.ts +279 -0
- package/src/native/types.ts +41 -238
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +1 -1
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +5 -6
- package/src/rust-ffi/context.ts +39 -10
- package/src/rust-ffi/index.ts +28 -17
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +1 -1
- package/src/rust-ffi/packed.ts +40 -2
- package/src/rust-ffi/proven.ts +16 -30
- package/src/rust-ffi/scalar/crypto.ts +188 -107
- package/src/rust-ffi/scalar/factories.ts +257 -27
- package/src/rust-ffi/scalar/hashing.ts +43 -41
- package/src/rust-ffi/scalar/http.ts +208 -77
- package/src/rust-ffi/scalar/index.ts +3 -3
- package/src/rust-ffi/scalar/interface.ts +88 -213
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +28 -12
- package/src/rust-ffi/scalar/payload.ts +159 -42
- package/src/rust-ffi/text.ts +31 -5
- package/src/selection.ts +27 -31
- package/src/shared/adaptive.ts +1 -2
- package/src/shared/buffer-pool.ts +72 -17
- package/src/shared/bytes.ts +75 -6
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +4 -4
- package/src/shared/log.ts +1 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/{packed.ts → packed/wire.ts} +31 -39
- package/src/shared/proven.ts +189 -496
- package/src/shared/runtime.ts +20 -25
- package/src/shared/uuid.ts +12 -28
- package/src/data/json-rows.ts +0 -33
- package/src/rust-ffi/batch.ts +0 -431
- package/src/shared/bench-classify.ts +0 -209
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Writing Rust for Bun — FFI Optimization Guide
|
|
2
|
+
|
|
3
|
+
> **Who this is for**: anyone (human or agent) writing or reviewing the Rust
|
|
4
|
+
> `extern "C"` surface in `rust/ffi/` and its `bun:ffi` binding in
|
|
5
|
+
> `src/native/ffi.ts`. The rules below are the concrete, citable behaviors of
|
|
6
|
+
> Bun's own `bun:ffi` implementation, distilled so that future generated Rust
|
|
7
|
+
> code is **optimized for Bun** out of the box — correct on the first `dlopen`,
|
|
8
|
+
> fast on the first call, and safe under burst load.
|
|
9
|
+
>
|
|
10
|
+
> **Version anchor**: behavior claims are verified against the **Rust-based Bun
|
|
11
|
+
> 1.3.14** source (`/home/adeel/poc/bun`). `bun:ffi` is experimental upstream;
|
|
12
|
+
> the bind-time self-test + napi fallback are what make it safe to use, and
|
|
13
|
+
> they are mandatory — not optional.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. How `bun:ffi` actually works (the call path)
|
|
18
|
+
|
|
19
|
+
`bun:ffi` is implemented natively in Bun's JavaScriptCore integration, not as a
|
|
20
|
+
JS shim:
|
|
21
|
+
|
|
22
|
+
- `dlopen(path, { symbols })` resolves each symbol via `dlsym`-style lookup and
|
|
23
|
+
creates a `JSFFIFunction` for it — a JS function that **holds the raw C
|
|
24
|
+
function pointer** and marshals arguments in-engine (`src/js/bun/ffi.ts`,
|
|
25
|
+
`src/runtime/ffi/ffi_body.rs::FFI::open`).
|
|
26
|
+
- **Hot call sites compile through the DFG/FTL JIT tiers into direct native
|
|
27
|
+
calls** — "argument conversion, arity handling, and result boxing happen
|
|
28
|
+
in-engine … with no per-argument JavaScript shim" (`docs/runtime/ffi.mdx`,
|
|
29
|
+
"Performance"). This is why the crossing is ~10–20 ns vs ~100–350 ns for a
|
|
30
|
+
Node-API call, and why Bun's own bench puts `bun:ffi` at 2–6× faster than
|
|
31
|
+
Node's N-API (upstream bench `bench/ffi/bun.js` in the Bun runtime checkout
|
|
32
|
+
`/home/adeel/poc/bun`, plus `docs/runtime/ffi.mdx`; castrum's own FFI-vs-napi
|
|
33
|
+
margin bench is `bench/ffi/ffi-margin.ts`).
|
|
34
|
+
- The `#[no_mangle] extern "C"` symbols come from the **same cdylib** that
|
|
35
|
+
napi-rs loads under Node — the two transports are two views of one binary.
|
|
36
|
+
|
|
37
|
+
**Implications for Rust code:**
|
|
38
|
+
|
|
39
|
+
- Keep `extern "C"` signatures **scalar + pointer/length**. Every argument is a
|
|
40
|
+
register-passable integer, float, or pointer — no variadics, no variadic
|
|
41
|
+
marshaling, no by-value structs (see §10).
|
|
42
|
+
- The ABI spec declared in the `dlopen` map (e.g. `["ptr","usize","u32"]`) MUST
|
|
43
|
+
match the Rust signature exactly — there is no runtime checking beyond the
|
|
44
|
+
bind-time self-test. A mismatch is a crash, not an error.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. The ABI type system (`FFIType`)
|
|
49
|
+
|
|
50
|
+
The numeric tags are pinned by static asserts in
|
|
51
|
+
`src/jsc/bindings/JSCFFIBridge.cpp` (do not rely on the numbers; rely on the
|
|
52
|
+
string names). Canonical names used by castrum's `dlopen` map:
|
|
53
|
+
|
|
54
|
+
| Rust type | `bun:ffi` name | Notes |
|
|
55
|
+
|-----------------|-------------------|-------|
|
|
56
|
+
| `u8` | `"u8"` | returns as JS `number` |
|
|
57
|
+
| `u32` | `"u32"` | returns as JS `number` |
|
|
58
|
+
| `usize` / `u64` | `"usize"`/`"u64"` | `usize == u64` on the host ABI; returns surface as **BigInt** unless `u64_fast` |
|
|
59
|
+
| `i64` | `"i64"` | args passed as `BigInt(...)`; returns as BigInt |
|
|
60
|
+
| `f64` | `"f64"` | e.g. `castrum_http_date_into(secs: f64, …)` |
|
|
61
|
+
| `*const u8` | `"ptr"` | TypedArray/DataView auto-converted to its pointer; a plain `number` is passed through |
|
|
62
|
+
| `*const u8` (NUL-terminated string) | `"cstring"` | args: JS string transcoded to a call-scoped NUL-terminated UTF-8 buffer (zero JS-side encode — the engine does it); returns: cloned JS string. **ARGS are the fast string-input path** (see §3 shape 4) |
|
|
63
|
+
| `*const u8` + length as a pair | `"buffer"` + `"buffer_length"` | see §4 |
|
|
64
|
+
| `void` | `"void"` | — |
|
|
65
|
+
| — | `"u64_fast"` / `"i64_fast"` | unboxed `number` when the value fits in a double (< 2^53); no BigInt boxing |
|
|
66
|
+
|
|
67
|
+
> Full table (incl. `i8..i32`, `f32`, `bool`, `char`, `function`, `napi_env`,
|
|
68
|
+
> `napi_value`) is in `docs/runtime/ffi.mdx` → "FFI types" and
|
|
69
|
+
> `packages/bun-types/ffi.d.ts`.
|
|
70
|
+
|
|
71
|
+
**Rule**: for any return that is a byte count / length / small scalar, use
|
|
72
|
+
`u64_fast` in the binding (castrum's `U64_FAST` constant) so the hot path never
|
|
73
|
+
boxes a BigInt. Keep the Rust return type as `usize`/`u32` — only the JS binding
|
|
74
|
+
changes.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 3. Canonical C-ABI signature shapes (use these)
|
|
79
|
+
|
|
80
|
+
Everything castrum exports follows one of four shapes. New exports should pick
|
|
81
|
+
the matching shape and nothing else:
|
|
82
|
+
|
|
83
|
+
1. **`(ptr, len) -> scalar`** — pure read of input, no output buffer:
|
|
84
|
+
`pub unsafe extern "C" fn castrum_crc32(data: *const u8, len: usize) -> u32`.
|
|
85
|
+
Bind as `{ args: inputAbi, returns: 'u32' }` (`inputAbi` = the
|
|
86
|
+
`buffer`/`buffer_length` pair or `(ptr,usize)`, see §4).
|
|
87
|
+
2. **`(ptr, len, out, out_cap) -> usize`** — output-into-caller-buffer with the
|
|
88
|
+
**needed-size convention** (§12): return the exact required byte count on a
|
|
89
|
+
too-small buffer, `0` = real error. Bind with `abi(['ptr','usize','ptr','usize'])`
|
|
90
|
+
and `returns: U64_FAST`, drive through `growExact`.
|
|
91
|
+
3. **`(…) -> *const c_char`** — cstring return (per-thread reused `CSTR_BUF`,
|
|
92
|
+
§7). Only for genuinely string-shaped results; prefer packed buffers for
|
|
93
|
+
structured data.
|
|
94
|
+
4. **`cstring` ARG for string inputs** — `f(s: *const c_char) -> …`. When the
|
|
95
|
+
input is TEXT (never NUL) and the JS caller has it as a JS `string`, pass it
|
|
96
|
+
as a `cstring` ARG instead of a `(ptr,len)` pair: the engine transcodes the
|
|
97
|
+
string to a call-scoped NUL-terminated UTF-8 buffer in-engine (zero JS-side
|
|
98
|
+
`encoder.encode`), and the callee borrows via `CStr::from_ptr(s).to_bytes()`
|
|
99
|
+
(no copy). **Benchmark-gated** (`bench:margin` `cstringArg` scenario):
|
|
100
|
+
~40–52 ns/call vs ~209–233 ns for `encodeUtf8` + `(ptr,len)` — a ~76–82% win,
|
|
101
|
+
including non-ASCII inputs. It is a LOSS for BYTE inputs (decode + re-encode)
|
|
102
|
+
— keep `(ptr,len)` there. **Verified `bun:ffi` facts**: JS `null` → NULL
|
|
103
|
+
pointer, and JS `''` (empty string) → NULL too — a present-but-empty value is
|
|
104
|
+
indistinguishable from absent through a `cstring` arg (so SSE's `Option`
|
|
105
|
+
event/id semantics stay on `(ptr,len)`). Adopted by: the validators
|
|
106
|
+
(`castrum_validate_*`), `castrum_ws_accept_key`, `castrum_mime_from_extension`,
|
|
107
|
+
`castrum_password_verify_bcrypt` (phc), `castrum_rate_limiter_check` (key),
|
|
108
|
+
and `castrum_ingress_handle_components` (url/ip).
|
|
109
|
+
|
|
110
|
+
Opaque instance handles are passed as a **bare `usize`** (`inner: usize`), and
|
|
111
|
+
must be null-checked at the top of the function (return the `0` sentinel) —
|
|
112
|
+
castrum's `castrum_ingress_handle_packed` / `castrum_*_matcher_matches` etc.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 4. `buffer` / `buffer_length` — the atomic snapshot pair
|
|
117
|
+
|
|
118
|
+
`FFIType.buffer` (20) and `FFIType.buffer_length` (21) are a paired ABI: pass
|
|
119
|
+
the **same** TypedArray/DataView for both, and the callee receives the pointer
|
|
120
|
+
and the **byte length** of that same view. The engine reads ptr + byteLength
|
|
121
|
+
off the same object **at call time** — an atomic snapshot you cannot get by
|
|
122
|
+
passing `view.byteLength` yourself (a JS-side length read can go stale against
|
|
123
|
+
a resizable/growable/transferred buffer). Source:
|
|
124
|
+
`packages/bun-types/ffi.d.ts` (`buffer_length` doc), `docs/runtime/ffi.mdx`.
|
|
125
|
+
|
|
126
|
+
- **Argument-only**: valid for `dlopen`/`linkSymbols`/`CFunction` args, NOT for
|
|
127
|
+
`cc()` or `JSCallback` (`src/runtime/ffi/ffi_body.rs::reject_cc_unsupported_types_error`).
|
|
128
|
+
- **castrum's pattern** (`src/native/ffi.ts`): `probeBufferLength()` verifies
|
|
129
|
+
the pair at bind time (an older Bun canary threw "invalid ABI type" for it);
|
|
130
|
+
when supported, the `abi()` transformer rewrites every `(ptr,usize)` pair to
|
|
131
|
+
`(buffer,buffer_length)` and call sites pass the view twice (`lenOrView`).
|
|
132
|
+
When not supported, explicit `(view, view.length)` is used. The probe makes
|
|
133
|
+
the fast path the default on every modern Bun (1.3.x+) with zero drift.
|
|
134
|
+
|
|
135
|
+
**Rule**: keep the probe. Never hardcode the pair as the only path — a future
|
|
136
|
+
Bun could remove it, and the probe + `(ptr,usize)` fallback is what keeps the
|
|
137
|
+
transport working.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 5. Pointers are `number`, not `BigInt`
|
|
142
|
+
|
|
143
|
+
Bun represents pointers as a JS **`number`** (`type Pointer = number` in
|
|
144
|
+
`packages/bun-types/ffi.d.ts`). 64-bit processors address ≤52 bits, which fits
|
|
145
|
+
in a double's 53-bit mantissa. Passing a `BigInt` to an FFI arg is converted to
|
|
146
|
+
a `number` anyway (`docs/runtime/ffi.mdx` → "Pointers"). Windows HANDLE values
|
|
147
|
+
are the exception — use `u64` for those.
|
|
148
|
+
|
|
149
|
+
- Get a pointer from a TypedArray with `ptr(view)`; read back with
|
|
150
|
+
`read.*` (fast, no DataView/ArrayBuffer alloc) or `toArrayBuffer`/`toBuffer`.
|
|
151
|
+
- **`u64`/`i64` returns** surface as BigInt (boxed) unless you bind
|
|
152
|
+
`u64_fast`/`i64_fast` (§2).
|
|
153
|
+
- **castrum's application**: the ingress opaque handle is obtained from napi as
|
|
154
|
+
a BigInt and converted to a number ONCE (`Number(handler.ingressInnerPtr())`
|
|
155
|
+
in `src/ingress/fast.ts` / `handlers.ts`), then passed as a `usize` arg —
|
|
156
|
+
number is cheaper than BigInt on the hot path.
|
|
157
|
+
|
|
158
|
+
**Rule**: pass opaque handles as `usize`/number; convert the napi BigInt once,
|
|
159
|
+
not per call. If a function could ever receive a value ≥ 2^53, it cannot be a
|
|
160
|
+
pointer — rethink the ABI.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 6. `cstring` returns are cloned at call time
|
|
165
|
+
|
|
166
|
+
When a symbol's return type is `cstring`, Bun clones the NUL-terminated C
|
|
167
|
+
string into a JS `string` at call time and returns that; `NULL` → `null`
|
|
168
|
+
(`src/js/bun/ffi.ts` `FFIBuilder`, `docs/runtime/ffi.mdx` → "Strings" +
|
|
169
|
+
"engine-native cstring" tests in `test/js/bun/ffi/ffi.test.js`). The engine
|
|
170
|
+
copies **nothing itself** — the C side must own the memory until the call
|
|
171
|
+
returns.
|
|
172
|
+
|
|
173
|
+
- **castrum's application**: the per-thread reused `CSTR_BUF` in `rust/ffi/`
|
|
174
|
+
(`cstring_return`) is exactly right — the clone is synchronous, so reusing
|
|
175
|
+
the buffer across calls is safe.
|
|
176
|
+
- **Aliasing caveat**: a C function that returns a pointer *derived from a
|
|
177
|
+
`cstring` argument* you passed as a JS string — that argument was transcoded
|
|
178
|
+
into an engine call-scoped buffer — is valid only until your **next** FFI
|
|
179
|
+
call reuses that buffer. Clone it rather than holding the raw address. Avoid
|
|
180
|
+
returning pointers into argument memory.
|
|
181
|
+
|
|
182
|
+
**Rule**: use per-thread reused buffers for cstring returns; never return a
|
|
183
|
+
pointer into JS-owned memory; treat `null` as the failure sentinel.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 7. `dlopen` lifetime — never `close()` while bound symbols live
|
|
188
|
+
|
|
189
|
+
Bun deliberately **leaks** a `dlopen`'d library if you never call `close()`:
|
|
190
|
+
"dlclose on GC is unsound because `.ptr` addresses escape the collector's view"
|
|
191
|
+
(`src/runtime/ffi/ffi_body.rs` — `FFI::finalize`). Each `JSFFIFunction` also
|
|
192
|
+
holds a reference to its owning FFI object so the dylib stays alive as long as
|
|
193
|
+
any bound function is reachable.
|
|
194
|
+
|
|
195
|
+
- **castrum's application**: `getBunFFI()` binds once, holds the symbols for
|
|
196
|
+
the process lifetime, and never calls `close()`. That is the correct pattern
|
|
197
|
+
— do not "clean up" by closing a live library. Closing is only for short-lived
|
|
198
|
+
ad-hoc libraries (e.g. a `dlopen` in a test), and only after no bound
|
|
199
|
+
function can be called.
|
|
200
|
+
|
|
201
|
+
**Rule**: bind once, hold forever. If you ever call `close()`, it must be after
|
|
202
|
+
all bound symbols are unreachable.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 8. Thread-safety & per-thread caches
|
|
207
|
+
|
|
208
|
+
`bun:ffi` symbols are shared across Worker threads (same dlopen'd cdylib
|
|
209
|
+
mapping; verified by castrum's `bench/ffi/ffi-workers.ts` — 4 workers, no
|
|
210
|
+
thread-safety issue). The Rust side must therefore keep any per-call mutable
|
|
211
|
+
state **thread-local**, never global:
|
|
212
|
+
|
|
213
|
+
- `HMAC_KEY_CACHE` (`thread_local! RefCell<LruCache<Vec<u8>, hmac::Key>>`) —
|
|
214
|
+
compiled-key cache with zero lock contention across threads.
|
|
215
|
+
- `CSTR_BUF` (`thread_local! Vec<u8>`) — cstring return buffer.
|
|
216
|
+
|
|
217
|
+
**Rule**: no `static mut`, no shared mutable global in the C-ABI path. Use
|
|
218
|
+
`thread_local!` for per-thread scratch/caches, or `OnceLock` for immutable
|
|
219
|
+
one-time state. Never hold a `Mutex` across an FFI call.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 9. There is no struct-by-value — use packed length-prefixed buffers
|
|
224
|
+
|
|
225
|
+
`bun:ffi`'s FFIType set has **no struct member** (only scalars, `ptr`,
|
|
226
|
+
`cstring`, `buffer`, `function`, and the napi types). You cannot pass a JS
|
|
227
|
+
object as a C struct argument, and a Rust `#[repr(C)]` struct is not directly
|
|
228
|
+
marshalable. This is why castrum's wire format is **packed, length-prefixed
|
|
229
|
+
byte buffers**:
|
|
230
|
+
|
|
231
|
+
- Inputs are packed by JS (`IngressInputPacker`, `pack_headers`, packed
|
|
232
|
+
parsers) into a `Uint8Array` and passed `(ptr, len)`.
|
|
233
|
+
- Outputs are written by Rust into a caller buffer as
|
|
234
|
+
`[u32 count][u32 field_len][field bytes]…` (or fixed-slot layouts like the
|
|
235
|
+
`IngressLayout` 38×`u32` blob / rate-limit verdict `[u8][u32][i64]`).
|
|
236
|
+
|
|
237
|
+
This also enables zero-copy sharing and buffer pooling — a struct-by-value ABI
|
|
238
|
+
would have forced per-call copies.
|
|
239
|
+
|
|
240
|
+
**Rule**: for any multi-field result, define a packed byte layout (fixed slots
|
|
241
|
+
or length-prefixed sections), declare it in ONE Rust place (e.g.
|
|
242
|
+
`rust/ingress/output.rs`), mirror it in JS (`src/ingress/constants.ts`), and pin
|
|
243
|
+
it with a unit test + bind-time self-test. **Alignment**: keep 8-byte scalar
|
|
244
|
+
fields (e.g. the `i64 reset_ms`) aligned to 8 in the layout so Rust reads are
|
|
245
|
+
unclipped and DataView reads are natural.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 10. Callbacks (`JSCallback`) — use sparingly, never in hot paths
|
|
250
|
+
|
|
251
|
+
`JSCallback` lets native code call back into JS. A `threadsafe` callback copies
|
|
252
|
+
the C args on the calling thread and **marshals the invocation onto the JS
|
|
253
|
+
thread** — from C's point of view it is asynchronous, and the value returned to
|
|
254
|
+
C is unspecified (treat it as `void`) (`docs/runtime/ffi.mdx` → "Callbacks",
|
|
255
|
+
`src/jsc/bindings/JSCFFIBridge.cpp::Bun__jscFFITimeadDispatch`). Async functions
|
|
256
|
+
are not supported. Each crossing has marshaling + event-loop cost.
|
|
257
|
+
|
|
258
|
+
**Rule**: prefer a synchronous return over a callback. If a callback is
|
|
259
|
+
unavoidable (push/event-driven integrations), pass `JSCallback.prototype.ptr`
|
|
260
|
+
directly (slightly faster than the object), declare `returns: "void"`-ish on the
|
|
261
|
+
C side, and call `close()` when done. castrum's hot request path uses **zero**
|
|
262
|
+
callbacks — keep it that way.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 11. Mandatory rules for every new Rust export
|
|
267
|
+
|
|
268
|
+
These are not optional — they are what make the C-ABI surface safe on Bun (no
|
|
269
|
+
napi-style unwind guard exists for raw `extern "C"`).
|
|
270
|
+
|
|
271
|
+
1. **`panic_guard` every fallible/allocating core** (`std::panic::catch_unwind`
|
|
272
|
+
in `rust/ffi/`, `AssertUnwindSafe` for captured `&mut [u8]`). An uncaught
|
|
273
|
+
panic unwinding through `extern "C"` kills the whole Bun process — this is
|
|
274
|
+
what happened under `11-concurrent-burst`.
|
|
275
|
+
2. **Bind-time self-test for EVERY new symbol** (`src/native/ffi/selfTest`).
|
|
276
|
+
`bun:ffi` is experimental upstream; the self-test + napi fallback is the
|
|
277
|
+
safety net. No symbol ships without a self-test vector.
|
|
278
|
+
3. **Needed-size convention** for variable-size outputs: return the EXACT
|
|
279
|
+
required byte count on a too-small buffer; `0` stays a REAL error. JS uses
|
|
280
|
+
`growExact` (allocate once, retry once — never a doubling re-run).
|
|
281
|
+
4. **Null-check every pointer arg** (`if data.is_null() && len != 0 { return 0 }`)
|
|
282
|
+
and every opaque handle (`inner == 0` → sentinel).
|
|
283
|
+
5. **`u64_fast` byte-count returns** so the JS side avoids BigInt boxing.
|
|
284
|
+
6. **Thread-local scratch** — no global mutable state (§8).
|
|
285
|
+
7. **Parity**: add the symbol to the `dlopen` map, `ffi/types.ts` (`BunFFI`),
|
|
286
|
+
the scalar builder, and `ffi-symbol-parity.test.ts`; a Rust unit test +
|
|
287
|
+
self-test; cross-check FFI vs napi byte-for-byte in `ffi.test.ts`.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 12. Bun's own Rust hygiene (borrowed rules)
|
|
292
|
+
|
|
293
|
+
Bun's Rust codebase (`/home/adeel/poc/bun`, AGENTS.md) enforces conventions
|
|
294
|
+
that transfer directly to castrum's cdylib:
|
|
295
|
+
|
|
296
|
+
- **RAII / `Drop` over manual cleanup**; don't leak or free twice across the
|
|
297
|
+
FFI boundary.
|
|
298
|
+
- **No mutable `#[no_mangle]` global state** without a synchronization
|
|
299
|
+
wrapper. Bun wraps exported statics in `RacyCell` (see
|
|
300
|
+
`src/jsc/FFI.rs`: `#[unsafe(no_mangle)] static ValueUndefined: RacyCell<…>`)
|
|
301
|
+
so the symbol bytes stay identical while satisfying `Sync`. castrum's
|
|
302
|
+
`thread_local!` caches and `OnceLock` state follow the same spirit.
|
|
303
|
+
- **`#[unsafe(no_mangle)]`** (Rust 2024) or `#[no_mangle]` (Rust 2021) on every
|
|
304
|
+
export, `pub unsafe extern "C"`, snake_case names, `///` doc comments.
|
|
305
|
+
- Keep napi types OUT of internal signatures so core logic stays unit-testable
|
|
306
|
+
in pure Rust (`#[cfg(test)] mod tests` in the same file).
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 13. Version anchors & verification
|
|
311
|
+
|
|
312
|
+
The behavioral claims above are verified against the Rust-based Bun source
|
|
313
|
+
checkout at `/home/adeel/poc/bun` (LATEST file: 1.3.14; `package.json` tracks
|
|
314
|
+
the 1.4 development line). The decision matrix and ADR-0003 measure against
|
|
315
|
+
the installed **Bun 1.4.0** runtime — both anchors describe the same
|
|
316
|
+
Rust-based runtime family. Castrum's own measurements live in `bench/`.
|
|
317
|
+
|
|
318
|
+
| Claim | Source (Bun checkout `/home/adeel/poc/bun`) |
|
|
319
|
+
|---|---|
|
|
320
|
+
| JIT-to-direct-native-call, 2–6× vs NAPI | `docs/runtime/ffi.mdx` → Performance; `bench/ffi/bun.js` (upstream) |
|
|
321
|
+
| `FFIType` numeric tags (`buffer`=20, `buffer_length`=21, `u64_fast`=16) | `src/js/bun/ffi.ts`; static asserts in `src/jsc/bindings/JSCFFIBridge.cpp` |
|
|
322
|
+
| `buffer`/`buffer_length` atomic snapshot, arg-only | `packages/bun-types/ffi.d.ts`; `src/runtime/ffi/ffi_body.rs::reject_cc_unsupported_types_error` |
|
|
323
|
+
| Pointers are `number`; BigInt converted to number | `docs/runtime/ffi.mdx` → Pointers |
|
|
324
|
+
| cstring returns cloned; NULL → null | `src/js/bun/ffi.ts` `FFIBuilder`; `test/js/bun/ffi/ffi.test.js` |
|
|
325
|
+
| dlopen leaks unless `close()`; unsound dlclose-on-GC | `src/runtime/ffi/ffi_body.rs` `FFI::finalize` |
|
|
326
|
+
| Low-overhead C-fn call (TinyCC path), zero bounds checking | `src/jsc/bindings/JSFFIFunction.h` |
|
|
327
|
+
|
|
328
|
+
**Verify after any FFI change**:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
nm -D --defined-only <addon> | grep -c castrum_ # = 85
|
|
332
|
+
bun run bench:http:smoke # after touching decoders/handlers
|
|
333
|
+
bun test test/unit/native/ffi.test.ts # FFI↔napi parity + self-test
|
|
334
|
+
bun test test/unit/native/ffi-symbol-parity.test.ts
|
|
335
|
+
```
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -58,9 +58,12 @@ Everything is **synchronous** — there is no async/await in the FFI surface.
|
|
|
58
58
|
|
|
59
59
|
> **Transport**: under Bun these calls run through `bun:ffi` (the PRIMARY
|
|
60
60
|
> transport — see `rust.transport()` / `rust.ffiActive()`); Node and any
|
|
61
|
-
> `CASTRUM_FFI_MODE=napi` fall back to the napi addon.
|
|
62
|
-
>
|
|
63
|
-
>
|
|
61
|
+
> `CASTRUM_FFI_MODE=napi` fall back to the napi addon. Since Phase 6 the
|
|
62
|
+
> compiled-once instances are NOT napi-only — their hot per-call ops route
|
|
63
|
+
> through the C-ABI via opaque-`usize` handles. `rust.text.*`
|
|
64
|
+
> (`mimeFromExtension`, `wsAcceptKey`, `validateEmail`/`validateUuid`/…) and
|
|
65
|
+
> the DOM `jsonParse` are FFI-first today; still napi-only:
|
|
66
|
+
> `rust.batch.*` and `rust.packed.*`.
|
|
64
67
|
|
|
65
68
|
> **Tip**: `rust` is a singleton created with sensible defaults. If you need
|
|
66
69
|
> isolated state (own rayon threads, own HMAC cache), call
|
|
@@ -85,28 +88,9 @@ loader.run("jsonPatch", docs, patches); // paired ops (companion array
|
|
|
85
88
|
loader.schema(validator)(docs); // schema bitset (+ .count())
|
|
86
89
|
```
|
|
87
90
|
|
|
88
|
-
See
|
|
89
|
-
`createLoader` options.
|
|
91
|
+
See docs/API.md — [Higher-order data loader](API.md#higher-order-data-loader-loader--createloader) — for the full op table and `createLoader` options.
|
|
90
92
|
|
|
91
|
-
## 3.
|
|
92
|
-
|
|
93
|
-
Not every `rust.*` function beats the JS baseline on every CPU. `proven`
|
|
94
|
-
exposes the **full** `rust.*` surface, and each function's JSDoc records its
|
|
95
|
-
measured performance vs the JS baseline — functions that are slower carry
|
|
96
|
-
`@deprecated`:
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
import { proven, PROVEN_SURFACE } from "castrum";
|
|
100
|
-
|
|
101
|
-
proven.fnv1a64(bytes); // JSDoc: ~11x faster than the JS baseline
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
`PROVEN_SURFACE` is the registry (pure data); `scripts/check-proven.ts`
|
|
105
|
-
audits it against the CPU benchmark so it can't drift, and
|
|
106
|
-
`scripts/annotate-performance.ts` (`bun run check:annotate`) refreshes the
|
|
107
|
-
per-function JSDoc from the latest report.
|
|
108
|
-
|
|
109
|
-
## 4. Stand up an HTTP server (the ingress pipeline)
|
|
93
|
+
## 3. Stand up an HTTP server (the ingress pipeline)
|
|
110
94
|
|
|
111
95
|
The quickest path is the **pre-baked** route handlers + the server builder:
|
|
112
96
|
|
|
@@ -138,7 +122,8 @@ console.log("listening on", server.port);
|
|
|
138
122
|
route options. The underlying factories (`readHandler`, `jsonWriteHandler`,
|
|
139
123
|
`echoHandler`, `headHandler`, `fallbackHandler`) are also exported directly
|
|
140
124
|
if you'd rather wire methods by hand.
|
|
141
|
-
- **
|
|
125
|
+
- **Runtime-adaptive**: `createIngressServer` picks `Bun.serve` on Bun and
|
|
126
|
+
`node:http` on Node; `createIngressServerNode` is the explicit Node adapter
|
|
142
127
|
with the same `routes` spec (and `await srv.ready` for the port).
|
|
143
128
|
|
|
144
129
|
The wire format is `{"ok":true,...,"requestId":...}` on success and
|
|
@@ -149,10 +134,10 @@ The wire format is `{"ok":true,...,"requestId":...}` on success and
|
|
|
149
134
|
> absolute lowest overhead and are comfortable calling `run()` yourself with a
|
|
150
135
|
> synchronous callback.
|
|
151
136
|
|
|
152
|
-
##
|
|
137
|
+
## 4. Run the benchmarks
|
|
153
138
|
|
|
154
139
|
```bash
|
|
155
|
-
# CPU benchmark (correctness checks + comparisons)
|
|
140
|
+
# CPU benchmark (correctness checks + comparisons)
|
|
156
141
|
bun run check
|
|
157
142
|
|
|
158
143
|
# HTTP benchmark across all servers (bun / elysia / ingress)
|
|
@@ -164,7 +149,7 @@ bun run bench:http:smoke
|
|
|
164
149
|
|
|
165
150
|
Results land in `bench/results/` (gitignored).
|
|
166
151
|
|
|
167
|
-
##
|
|
152
|
+
## 5. Run the tests
|
|
168
153
|
|
|
169
154
|
```bash
|
|
170
155
|
# Everything at once (TS unit + Rust unit + typecheck + lint)
|
|
@@ -177,17 +162,14 @@ bun run lint:ci # Biome lint gate (what CI enforces)
|
|
|
177
162
|
bun run test:node # Node.js integration tests (node --test) — run `bun run build:js` first
|
|
178
163
|
```
|
|
179
164
|
|
|
180
|
-
##
|
|
165
|
+
## 6. Make your first change
|
|
181
166
|
|
|
182
167
|
1. Find where the code lives with [`REPO_MAP.md`](./REPO_MAP.md).
|
|
183
168
|
2. Edit the TypeScript (`src/`) and/or Rust (`rust/`).
|
|
184
169
|
3. If you changed Rust, rebuild the addon: `bun run build:debug`.
|
|
185
170
|
4. Run `bun test`, `bun run typecheck`, and `cargo test`.
|
|
186
171
|
5. If you touched any server or `handlers.ts`, run `bun run bench:http:smoke`.
|
|
187
|
-
6.
|
|
188
|
-
(`src/shared/proven.ts`) and run `bun run check:proven:fail` on a release
|
|
189
|
-
build (`bun run build`).
|
|
190
|
-
7. Format + lint: `bun run format` and `bun run lint`.
|
|
172
|
+
6. Format + lint: `bun run format` and `bun run lint`.
|
|
191
173
|
|
|
192
174
|
## Common gotchas
|
|
193
175
|
|
|
@@ -203,7 +185,9 @@ bun run test:node # Node.js integration tests (node --test) — run `bun run bui
|
|
|
203
185
|
|
|
204
186
|
## Next steps
|
|
205
187
|
|
|
206
|
-
- [`
|
|
188
|
+
- [`API.md`](./API.md) — the complete public API reference
|
|
207
189
|
- [`INGRESS.md`](./INGRESS.md) — the full pre-baked ingress API
|
|
190
|
+
- [`CASE_STUDY.md`](./CASE_STUDY.md) — the story behind the numbers (how castrum was built)
|
|
191
|
+
- [`REPO_MAP.md`](./REPO_MAP.md) — where everything is and how it connects
|
|
208
192
|
- [`ARCHITECTURE.md`](./ARCHITECTURE.md) — deep-dive internals
|
|
209
193
|
- [`AGENTS.md`](../AGENTS.md) — agent/contributor guidance
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# HOW TO ADD AN OP (runbook)
|
|
2
|
+
|
|
3
|
+
This is the single runbook for adding a new native operation to `castrum` end
|
|
4
|
+
to end. It consolidates the six places a new op must be wired, so a contributor
|
|
5
|
+
(or AI agent) knows exactly what to touch and in what order. "Op" = a
|
|
6
|
+
performance-critical primitive (`crc32`, `hmacSha256`, `jsonValid`,
|
|
7
|
+
`gzipCompress`, `validateEmail`, …) exposed through the native addon.
|
|
8
|
+
|
|
9
|
+
The golden rule: **a new op must be reachable from the public `rust.*` surface,
|
|
10
|
+
bound on BOTH transports (bun:ffi primary + napi fallback), pinned by a
|
|
11
|
+
bind-time self-test, and listed in the op-selection metadata** — or the parity
|
|
12
|
+
guards fail.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 0. The six wiring points at a glance
|
|
17
|
+
|
|
18
|
+
| # | File | What you add |
|
|
19
|
+
|---|------|--------------|
|
|
20
|
+
| 1 | `rust/<domain>/<module>.rs` (+ `mod.rs`) | The pure-Rust core (no napi types — unit-testable) |
|
|
21
|
+
| 2 | `rust/ffi/<domain>.rs` (+ `rust/ffi/util.rs`) | The `#[no_mangle] extern "C"` export (`panic_guard`, correct ABI) |
|
|
22
|
+
| 3 | `src/native/ffi.ts` + `src/native/ffi/types.ts` + `src/native/ffi/build/<domain>.ts` | The `bun:ffi` dlopen map entry + `BunFFI` method + per-domain self-test check |
|
|
23
|
+
| 4 | `src/rust-ffi/scalar/<area>.ts` (or `text`/`batch`/`packed`) | The public `rust.*` wrapper |
|
|
24
|
+
| 5 | `src/selection.json` + `src/shared/proven.ts` + `src/selection.ts` | The baked op-selection / delegation metadata |
|
|
25
|
+
| 6 | `test/unit/native/ffi.test.ts` + `test/unit/contract/wiring.test.ts` | The parity tests |
|
|
26
|
+
|
|
27
|
+
Follow the numbered sections below. When you're done, the parity gates
|
|
28
|
+
(§7) are your safety net.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Add a new Rust module (the core)
|
|
33
|
+
|
|
34
|
+
Create the pure-Rust core in its domain folder, e.g. `rust/crypto/hmac_sha256.rs`.
|
|
35
|
+
Keep **napi types out of the core** — internal signatures use `&[u8]` /
|
|
36
|
+
`&str` / `Result<_, String>` so it stays unit-testable with plain `cargo test`.
|
|
37
|
+
|
|
38
|
+
```rust
|
|
39
|
+
/// Compute an FNV-1a 64-bit hash of `input`.
|
|
40
|
+
#[inline]
|
|
41
|
+
pub fn fnv1a64_bytes(input: &[u8]) -> u64 { /* ... */ }
|
|
42
|
+
|
|
43
|
+
#[cfg(test)]
|
|
44
|
+
mod tests {
|
|
45
|
+
use super::*;
|
|
46
|
+
#[test]
|
|
47
|
+
fn standard_vectors() { /* ... */ }
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Register the module in `rust/lib.rs` (the module map comment) and in the
|
|
52
|
+
folder's `mod.rs` re-exports.
|
|
53
|
+
- Ship `#[cfg(test)] mod tests` in the SAME file (cross-module tests live in
|
|
54
|
+
`rust/panic_safety.rs` / `rust/proptest_suite.rs`; shared helpers in
|
|
55
|
+
`rust/test_support.rs`).
|
|
56
|
+
- If it needs a napi-visible class or batch, add a thin `api.rs` boundary in
|
|
57
|
+
the module folder (name it `api`, **not** `napi` — that collides with the
|
|
58
|
+
`napi` crate under `use super::*`).
|
|
59
|
+
|
|
60
|
+
## 2. Expose it over the C ABI (bun:ffi primary)
|
|
61
|
+
|
|
62
|
+
Add the `#[no_mangle] extern "C"` export in `rust/ffi/<domain>.rs`. Every
|
|
63
|
+
fallible / allocating export **must** route its core through `panic_guard`
|
|
64
|
+
(defined in `rust/ffi/util.rs`) so an uncaught panic can't unwind through
|
|
65
|
+
`extern "C"` and kill the process.
|
|
66
|
+
|
|
67
|
+
Rules (see `docs/FFI_BUN_GUIDE.md` for the full contract):
|
|
68
|
+
- Keep signatures **scalar + `(ptr,len)`** — no struct-by-value, no variadics.
|
|
69
|
+
- Null-check pointers and opaque handles (return `0` / `null` instead of
|
|
70
|
+
dereferencing).
|
|
71
|
+
- Byte-count returns MUST use `u64_fast` (`U64_FAST` in ffi.ts); opaque handles
|
|
72
|
+
pass as `usize`/number.
|
|
73
|
+
- String ARGS come in as `*const c_char` (bun:ffi `cstring`); use
|
|
74
|
+
`CStr::from_ptr(header).to_bytes()`.
|
|
75
|
+
- String RETURNS use `cstring_return` (per-thread `CSTR_BUF`, cloned at call
|
|
76
|
+
time — never return a pointer into JS-owned memory).
|
|
77
|
+
- **Variable-size outputs**: return the EXACT required byte count on a
|
|
78
|
+
too-small buffer (0 = real error) so JS allocates once and retries at most
|
|
79
|
+
once.
|
|
80
|
+
|
|
81
|
+
```rust
|
|
82
|
+
/// FNV-1a 64-bit hash → u64.
|
|
83
|
+
#[no_mangle]
|
|
84
|
+
pub unsafe extern "C" fn castrum_fnv1a64(data: *const u8, len: usize) -> u64 {
|
|
85
|
+
if data.is_null() { return 0; }
|
|
86
|
+
panic_guard(
|
|
87
|
+
|| crate::crypto::hashing::fnv1a64_bytes(std::slice::from_raw_parts(data, len)),
|
|
88
|
+
0,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- A `#[no_mangle]` symbol emits regardless of module path — no re-export needed
|
|
94
|
+
for the C ABI itself (re-exports are only needed for unit tests to reach the
|
|
95
|
+
item).
|
|
96
|
+
|
|
97
|
+
## 3. Bind it on the bun:ffi path (transport core + build folder)
|
|
98
|
+
|
|
99
|
+
`src/native/ffi.ts` owns the `dlopen` symbol map (`bind()`). Add your symbol
|
|
100
|
+
key (the EXACT `castrum_*` name) to that map with its ABI spec.
|
|
101
|
+
|
|
102
|
+
Then add the ergonomic method where it belongs in the decomposed bind surface:
|
|
103
|
+
- `src/native/ffi/build/codecs.ts` — hashing / codec / crypto / auth
|
|
104
|
+
- `src/native/ffi/build/compress.ts` — gzip/brotli + jsonPatch
|
|
105
|
+
- `src/native/ffi/build/parse.ts` — packed parsers + media-type/http-date/accept
|
|
106
|
+
- `src/native/ffi/build/instances.ts` — opaque-handle evals (schema/jwt/template/route/ingress)
|
|
107
|
+
|
|
108
|
+
Add the method signature to the `BunFFI` interface in
|
|
109
|
+
`src/native/ffi/types.ts` (use a `Raw2..Raw12` / `RawCStr` signature for the
|
|
110
|
+
raw symbol, or add a new one if the arity doesn't fit).
|
|
111
|
+
|
|
112
|
+
**Add the self-test next to the binding**: each `build/*.ts` file exports a
|
|
113
|
+
`selfTest<Domain>` function; add a known-good vector check for your method
|
|
114
|
+
there. `src/native/ffi/selftest.ts` is a thin aggregator that ANDs all four
|
|
115
|
+
domain self-tests — a new symbol is bound **and** self-tested in one file.
|
|
116
|
+
|
|
117
|
+
## 4. Expose it on the public `rust.*` surface
|
|
118
|
+
|
|
119
|
+
`src/rust-ffi/scalar/<area>.ts` (or `text.ts` / `batch/` / `packed.ts`) builds
|
|
120
|
+
the public wrapper. The wrapper normalizes the string-vs-bytes return
|
|
121
|
+
divergence (`toBytes`/`toText` — `src/shared/bytes.ts`), adds the pooled
|
|
122
|
+
`*Into` variants where hot, and (optionally) delegates to Bun built-ins when
|
|
123
|
+
`isBun()` via `src/runtime/builtins.ts` (`BUILTIN_OPS`).
|
|
124
|
+
|
|
125
|
+
Add a JSDoc block (`@param`/`@returns`/`@example`) — `bun run check:jsdoc`
|
|
126
|
+
enforces a 95% floor on exported symbols.
|
|
127
|
+
|
|
128
|
+
## 5. Op-selection metadata (baked)
|
|
129
|
+
|
|
130
|
+
- `src/selection.json` — the machine-readable selection row for the op
|
|
131
|
+
(native/js/bun winner). Generated by `scripts/select-native.ts --write`.
|
|
132
|
+
- **GOTCHA**: `select-native.ts`'s `OPS` array is the source of truth for what
|
|
133
|
+
stays in `selection.json`. ANY op with a baked native/js decision MUST be
|
|
134
|
+
listed there (or as a `skip+pinned` entry) or `--write` DELETES it from the
|
|
135
|
+
selection. If you add an op to `rust/selection.rs`, add it to `OPS` too.
|
|
136
|
+
- `src/shared/proven.ts` — `PROVEN_SELECTION` (PURE DATA) maps each op to its
|
|
137
|
+
proven winner (`native` / `js` / `bun`). Add/update the entry.
|
|
138
|
+
- `src/selection.ts` — `BUN_WINS` delegation registry (which ops delegate to
|
|
139
|
+
Bun built-ins under Bun). Keep `src/runtime/builtins.ts` `BUILTIN_OPS` in
|
|
140
|
+
sync.
|
|
141
|
+
|
|
142
|
+
## 6. Parity tests
|
|
143
|
+
|
|
144
|
+
- `test/unit/native/ffi.test.ts` — pin the ffi-vs-napi byte-parity for the new
|
|
145
|
+
op (the transport self-test only guards bind-time vectors; this pins real
|
|
146
|
+
parity against a live napi instance).
|
|
147
|
+
- `test/unit/contract/wiring.test.ts` — verifies each baked `PROVEN_SELECTION`
|
|
148
|
+
winner is actually wired (`opImpl(op)` agrees, `builtins.has(op)` matches,
|
|
149
|
+
native/js entries match the addon's embedded `selection.json`).
|
|
150
|
+
- `test/unit/native/ffi-symbol-parity.test.ts` — scans `rust/ffi/` for every
|
|
151
|
+
`castrum_*` symbol and asserts it's bound in the ffi.ts map AND exercised by
|
|
152
|
+
the self-test. New symbol not bound → this fails.
|
|
153
|
+
|
|
154
|
+
## 7. Run the gates
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
cargo test # Rust cores + C-ABI tests
|
|
158
|
+
bun test # TS suite (incl. the parity tests)
|
|
159
|
+
bunx tsc --noEmit && bunx tsc --noEmit -p tsconfig.test.json
|
|
160
|
+
bun run lint # Biome
|
|
161
|
+
bun run check:jsdoc # ≥95% JSDoc on exported symbols
|
|
162
|
+
bun run check:clean # module headers, runtime seam, purity, doc links
|
|
163
|
+
bun run check:version # package.json ↔ Cargo.toml ↔ CHANGELOG
|
|
164
|
+
bun run bench:http:smoke # if you touched the ingress path
|
|
165
|
+
bun run bench:startup # if you touched the transport/loader
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Checklist
|
|
171
|
+
|
|
172
|
+
- [ ] Pure-Rust core in the right domain folder with `#[cfg(test)] mod tests`
|
|
173
|
+
- [ ] `#[no_mangle] extern "C"` export with `panic_guard` + null checks +
|
|
174
|
+
correct `u64_fast`/cstring ABI
|
|
175
|
+
- [ ] Symbol in the `ffi.ts` dlopen map + `BunFFI` interface method
|
|
176
|
+
- [ ] Per-domain `selfTest*` vector in the same `ffi/build/*.ts` file
|
|
177
|
+
- [ ] Public `rust.*` wrapper with JSDoc (+ `*Into` pooled variant if hot)
|
|
178
|
+
- [ ] `selection.json` + `PROVEN_SELECTION` + `BUILTIN_OPS` (if delegating)
|
|
179
|
+
- [ ] `ffi.test.ts` + `wiring.test.ts` parity coverage
|
|
180
|
+
- [ ] All §7 gates green
|