castrum 0.9.3 → 0.9.6

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.
Files changed (206) hide show
  1. package/Cargo.lock +2242 -0
  2. package/Cargo.toml +155 -0
  3. package/README.md +14 -6
  4. package/build.rs +5 -0
  5. package/castrum.darwin-arm64.node +0 -0
  6. package/castrum.darwin-x64.node +0 -0
  7. package/castrum.linux-arm64-gnu.node +0 -0
  8. package/castrum.linux-arm64-musl.node +0 -0
  9. package/castrum.linux-x64-gnu.node +0 -0
  10. package/castrum.linux-x64-musl.node +0 -0
  11. package/castrum.linux-x64-v3-gnu.node +0 -0
  12. package/castrum.win32-x64-msvc.node +0 -0
  13. package/dist/index.d.ts +1508 -1291
  14. package/dist/index.js +3391 -2069
  15. package/docs/API.md +2 -1
  16. package/docs/ARCHITECTURE.md +3 -2
  17. package/docs/CASE_STUDY.md +5 -3
  18. package/docs/ENVIRONMENT.md +25 -2
  19. package/docs/FFI_BUN_GUIDE.md +242 -14
  20. package/docs/HOW_TO_ADD_AN_OP.md +10 -0
  21. package/docs/REPO_MAP.md +17 -6
  22. package/docs/RND-CONCURRENCY.md +772 -0
  23. package/docs/bun-builtins-decision-matrix.md +4 -1
  24. package/index.ts +24 -25
  25. package/package.json +173 -156
  26. package/rust/crypto/aead.rs +378 -0
  27. package/rust/crypto/argon2.rs +265 -0
  28. package/rust/crypto/base64.rs +420 -0
  29. package/rust/crypto/bcrypt.rs +89 -0
  30. package/rust/crypto/cookie_sign.rs +169 -0
  31. package/rust/crypto/csrf.rs +106 -0
  32. package/rust/crypto/ed25519.rs +215 -0
  33. package/rust/crypto/hashing.rs +140 -0
  34. package/rust/crypto/hmac_sha256.rs +88 -0
  35. package/rust/crypto/jwt/api.rs +286 -0
  36. package/rust/crypto/jwt/mod.rs +22 -0
  37. package/rust/crypto/jwt/tests.rs +312 -0
  38. package/rust/crypto/jwt/token.rs +360 -0
  39. package/rust/crypto/mod.rs +27 -0
  40. package/rust/crypto/pbkdf2.rs +105 -0
  41. package/rust/crypto/random_token.rs +85 -0
  42. package/rust/crypto/session.rs +222 -0
  43. package/rust/ffi/crypto.rs +708 -0
  44. package/rust/ffi/hashing.rs +285 -0
  45. package/rust/ffi/http.rs +752 -0
  46. package/rust/ffi/ingress.rs +303 -0
  47. package/rust/ffi/json.rs +517 -0
  48. package/rust/ffi/jwt.rs +419 -0
  49. package/rust/ffi/metrics.rs +373 -0
  50. package/rust/ffi/mod.rs +69 -0
  51. package/rust/ffi/payload.rs +275 -0
  52. package/rust/ffi/probe.rs +60 -0
  53. package/rust/ffi/rate_limit.rs +77 -0
  54. package/rust/ffi/route.rs +85 -0
  55. package/rust/ffi/task.rs +172 -0
  56. package/rust/ffi/tests.rs +2333 -0
  57. package/rust/ffi/util.rs +152 -0
  58. package/rust/ffi/validators.rs +123 -0
  59. package/rust/http/accept.rs +606 -0
  60. package/rust/http/cookie_parser.rs +140 -0
  61. package/rust/http/etag.rs +289 -0
  62. package/rust/http/form.rs +136 -0
  63. package/rust/http/headers.rs +267 -0
  64. package/rust/http/http_date.rs +286 -0
  65. package/rust/http/http_parser.rs +252 -0
  66. package/rust/http/media_type.rs +355 -0
  67. package/rust/http/method.rs +200 -0
  68. package/rust/http/mime_lookup.rs +130 -0
  69. package/rust/http/mod.rs +32 -0
  70. package/rust/http/multipart.rs +685 -0
  71. package/rust/http/query_parser.rs +211 -0
  72. package/rust/http/url_codec.rs +296 -0
  73. package/rust/http/url_join.rs +324 -0
  74. package/rust/ingress/api.rs +355 -0
  75. package/rust/ingress/cors.rs +403 -0
  76. package/rust/ingress/ingress_constants.rs +209 -0
  77. package/rust/ingress/ip_trust.rs +312 -0
  78. package/rust/ingress/mod.rs +48 -0
  79. package/rust/ingress/native_route.rs +770 -0
  80. package/rust/ingress/options.rs +176 -0
  81. package/rust/ingress/output.rs +406 -0
  82. package/rust/ingress/packed.rs +191 -0
  83. package/rust/ingress/pipeline.rs +534 -0
  84. package/rust/ingress/proxy.rs +138 -0
  85. package/rust/ingress/rate_limit.rs +538 -0
  86. package/rust/ingress/terminal.rs +114 -0
  87. package/rust/ingress/tests.rs +567 -0
  88. package/rust/ingress/time.rs +70 -0
  89. package/rust/json/fast_schema/capture.rs +336 -0
  90. package/rust/json/fast_schema/compile.rs +556 -0
  91. package/rust/json/fast_schema/cursor.rs +368 -0
  92. package/rust/json/fast_schema/email.rs +282 -0
  93. package/rust/json/fast_schema/errors.rs +247 -0
  94. package/rust/json/fast_schema/mod.rs +57 -0
  95. package/rust/json/fast_schema/tests.rs +1143 -0
  96. package/rust/json/fast_schema/types.rs +140 -0
  97. package/rust/json/fast_schema/validate.rs +931 -0
  98. package/rust/json/json_ops.rs +121 -0
  99. package/rust/json/json_schema.rs +837 -0
  100. package/rust/json/json_ser.rs +883 -0
  101. package/rust/json/mod.rs +19 -0
  102. package/rust/json/napi_marshal.rs +318 -0
  103. package/rust/json/patch/api.rs +60 -0
  104. package/rust/json/patch/engine.rs +153 -0
  105. package/rust/json/patch/mod.rs +47 -0
  106. package/rust/json/patch/ops.rs +270 -0
  107. package/rust/json/patch/pointer.rs +49 -0
  108. package/rust/json/patch/tests.rs +499 -0
  109. package/rust/lib.rs +73 -0
  110. package/rust/metrics/api.rs +148 -0
  111. package/rust/metrics/mod.rs +21 -0
  112. package/rust/metrics/registry.rs +961 -0
  113. package/rust/panic_safety.rs +40 -0
  114. package/rust/payload/compress.rs +512 -0
  115. package/rust/payload/mod.rs +13 -0
  116. package/rust/payload/sse.rs +214 -0
  117. package/rust/payload/template.rs +253 -0
  118. package/rust/payload/websocket.rs +69 -0
  119. package/rust/payload/ws_frames.rs +354 -0
  120. package/rust/proptest_suite.rs +139 -0
  121. package/rust/selection.rs +95 -0
  122. package/rust/src/bin/native-bench.rs +495 -0
  123. package/rust/task/completion.rs +119 -0
  124. package/rust/task/mod.rs +65 -0
  125. package/rust/task/ops.rs +503 -0
  126. package/rust/task/runtime.rs +220 -0
  127. package/rust/task/tests.rs +231 -0
  128. package/rust/test_support.rs +71 -0
  129. package/rust/util/batch/api.rs +266 -0
  130. package/rust/util/batch/core.rs +473 -0
  131. package/rust/util/batch/mod.rs +23 -0
  132. package/rust/util/batch/tests.rs +445 -0
  133. package/rust/util/batch_core.rs +184 -0
  134. package/rust/util/bytes.rs +594 -0
  135. package/rust/util/mod.rs +34 -0
  136. package/rust/util/packed.rs +473 -0
  137. package/rust/util/text.rs +130 -0
  138. package/rust/util/threadpool.rs +128 -0
  139. package/rust/util/validation.rs +419 -0
  140. package/rust-toolchain.toml +17 -0
  141. package/scripts/postinstall.mjs +199 -0
  142. package/src/ingress/constants.ts +1 -1
  143. package/src/ingress/context.ts +2 -2
  144. package/src/ingress/decode/baked-result.ts +13 -13
  145. package/src/ingress/decode/fast-result.ts +17 -17
  146. package/src/ingress/errors.ts +7 -7
  147. package/src/ingress/headers/baked-templates.ts +1 -1
  148. package/src/ingress/headers/fast-templates.ts +3 -3
  149. package/src/ingress/health.ts +1 -2
  150. package/src/ingress/index.ts +1 -1
  151. package/src/ingress/native-route.ts +9 -5
  152. package/src/ingress/options.ts +1 -1
  153. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  154. package/src/ingress/packing/route-wire.ts +1 -5
  155. package/src/ingress/response/error-bodies.ts +5 -5
  156. package/src/ingress/response/terminal.ts +1 -1
  157. package/src/ingress/routes/echo.ts +4 -4
  158. package/src/ingress/routes/fallback.ts +2 -2
  159. package/src/ingress/routes/head.ts +2 -2
  160. package/src/ingress/routes/native.ts +2 -1
  161. package/src/ingress/routes/options.ts +1 -1
  162. package/src/ingress/routes/read.ts +2 -2
  163. package/src/ingress/server.ts +10 -6
  164. package/src/ingress/status.ts +7 -7
  165. package/src/integration/batch.ts +1 -1
  166. package/src/integration/index.ts +9 -9
  167. package/src/integration/pipeline.ts +6 -6
  168. package/src/integration/websocket.ts +2 -2
  169. package/src/loader/index.ts +1 -5
  170. package/src/loader/ops.ts +1 -1
  171. package/src/native/ffi/build/codecs.ts +130 -12
  172. package/src/native/ffi/build/compress.ts +22 -10
  173. package/src/native/ffi/build/instances.ts +46 -4
  174. package/src/native/ffi/build/metrics.ts +247 -0
  175. package/src/native/ffi/build/parse.ts +59 -23
  176. package/src/native/ffi/build/task.ts +79 -0
  177. package/src/native/ffi/build/util.ts +32 -1
  178. package/src/native/ffi/build.ts +4 -0
  179. package/src/native/ffi/selftest.ts +12 -3
  180. package/src/native/ffi/types.ts +151 -9
  181. package/src/native/ffi.ts +160 -15
  182. package/src/native/index.ts +1 -1
  183. package/src/native/loader.ts +55 -5
  184. package/src/native/types/instances.ts +34 -0
  185. package/src/native/types.ts +11 -0
  186. package/src/runtime/codec.ts +53 -9
  187. package/src/runtime/types.ts +7 -0
  188. package/src/rust-ffi/batch/build.ts +16 -9
  189. package/src/rust-ffi/index.ts +7 -6
  190. package/src/rust-ffi/options.ts +1 -3
  191. package/src/rust-ffi/proven.ts +3 -3
  192. package/src/rust-ffi/scalar/crypto.ts +1 -1
  193. package/src/rust-ffi/scalar/factories.ts +73 -0
  194. package/src/rust-ffi/scalar/hashing.ts +1 -1
  195. package/src/rust-ffi/scalar/http.ts +71 -27
  196. package/src/rust-ffi/scalar/index.ts +2 -0
  197. package/src/rust-ffi/scalar/interface.ts +20 -0
  198. package/src/rust-ffi/scalar/payload.ts +11 -3
  199. package/src/rust-ffi/scalar/validation.ts +121 -0
  200. package/src/rust-ffi/text.ts +25 -3
  201. package/src/selection.ts +1 -1
  202. package/src/shared/bytes.ts +24 -4
  203. package/src/shared/codec.ts +18 -3
  204. package/src/task/index.ts +24 -0
  205. package/src/task/op.ts +171 -0
  206. package/src/task/runtime.ts +568 -0
@@ -0,0 +1,275 @@
1
+ // rust/ffi/payload.rs — payload C-ABI exports.
2
+ //
3
+ // WebSocket accept-key + frame encode/decode, gzip/brotli compress-decompress
4
+ // (via the shared `compress_to_out!` macro — see the needed-size convention),
5
+ // and SSE event encoding.
6
+
7
+ use std::slice;
8
+
9
+ use super::util::{cstring_return, panic_guard};
10
+
11
+ /// RFC 6455 Sec-WebSocket-Accept (28 bytes) returned as a null-terminated C
12
+ /// string into the per-thread reused buffer (`cstring` return).
13
+ ///
14
+ /// `key` is a `bun:ffi` `cstring` ARG — the engine transcodes the JS base64 key
15
+ /// in-engine (JS does zero encode; the callee borrows via `CStr::from_ptr`).
16
+ /// The pooled `castrum_ws_accept_key_into` keeps the `(ptr,len)` byte form.
17
+ ///
18
+ /// # Safety
19
+ /// `key` must be a valid NUL-terminated C string.
20
+ #[no_mangle]
21
+ pub unsafe extern "C" fn castrum_ws_accept_key(
22
+ key: *const std::os::raw::c_char,
23
+ ) -> *const std::os::raw::c_char {
24
+ if key.is_null() {
25
+ return std::ptr::null();
26
+ }
27
+ cstring_return(28, |out| {
28
+ crate::payload::websocket::ws_accept_key_into(std::ffi::CStr::from_ptr(key).to_bytes(), out)
29
+ .ok()
30
+ })
31
+ }
32
+
33
+ /// RFC 6455 Sec-WebSocket-Accept written directly into a caller buffer — the
34
+ /// pooled sibling of `castrum_ws_accept_key` (no cstring round-trip). Writes 28
35
+ /// bytes; returns bytes written, the exact required size when `out_cap` is too
36
+ /// small, or 0 on a malformed key.
37
+ ///
38
+ /// # Safety
39
+ /// `key` must be valid for reads of `klen` bytes; `out` for writes up to `out_cap`.
40
+ #[no_mangle]
41
+ pub unsafe extern "C" fn castrum_ws_accept_key_into(
42
+ key: *const u8,
43
+ klen: usize,
44
+ out: *mut u8,
45
+ out_cap: usize,
46
+ ) -> usize {
47
+ if key.is_null() || out.is_null() {
48
+ return 0;
49
+ }
50
+ if out_cap < 28 {
51
+ return 28;
52
+ }
53
+ match crate::payload::websocket::ws_accept_key_into(
54
+ slice::from_raw_parts(key, klen),
55
+ slice::from_raw_parts_mut(out, 28),
56
+ ) {
57
+ Ok(28) => 28,
58
+ _ => 0,
59
+ }
60
+ }
61
+
62
+ /// WebSocket frame encode into `out` (opcode 1/2/8/9/10, `mask`/`fin` flags).
63
+ ///
64
+ /// # Safety
65
+ /// `payload` must be valid for reads of `plen` bytes; `out` for `out_cap`.
66
+ #[no_mangle]
67
+ pub unsafe extern "C" fn castrum_ws_frame_encode(
68
+ opcode: u8,
69
+ payload: *const u8,
70
+ plen: usize,
71
+ mask: u8,
72
+ fin: u8,
73
+ out: *mut u8,
74
+ out_cap: usize,
75
+ ) -> usize {
76
+ if payload.is_null() || out.is_null() {
77
+ return 0;
78
+ }
79
+ crate::payload::ws_frames::encode_frame_into(
80
+ opcode,
81
+ slice::from_raw_parts(payload, plen),
82
+ mask != 0,
83
+ fin != 0,
84
+ slice::from_raw_parts_mut(out, out_cap),
85
+ )
86
+ .unwrap_or(0)
87
+ }
88
+
89
+ /// Decode a WebSocket frame into a packed `[u8 flags][u8 opcode][u32 payload_len]
90
+ /// [payload]` layout (flags bit0 = FIN). Returns bytes written, 0 on malformed
91
+ /// input. JS decodes the small header into the `WsFrame` shape.
92
+ ///
93
+ /// # Safety
94
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
95
+ #[no_mangle]
96
+ pub unsafe extern "C" fn castrum_ws_frame_decode_packed(
97
+ data: *const u8,
98
+ len: usize,
99
+ out: *mut u8,
100
+ out_cap: usize,
101
+ ) -> usize {
102
+ if data.is_null() || out.is_null() {
103
+ return 0;
104
+ }
105
+ let Some(frame) = crate::payload::ws_frames::decode_frame(slice::from_raw_parts(data, len))
106
+ else {
107
+ return 0;
108
+ };
109
+ let Some(need) = 6usize.checked_add(frame.payload.len()) else {
110
+ return 0;
111
+ };
112
+ if need > out_cap {
113
+ return 0;
114
+ }
115
+ let o = slice::from_raw_parts_mut(out, need);
116
+ o[0] = u8::from(frame.fin);
117
+ o[1] = frame.opcode;
118
+ o[2..6].copy_from_slice(&(frame.payload.len() as u32).to_le_bytes());
119
+ o[6..].copy_from_slice(&frame.payload);
120
+ need
121
+ }
122
+
123
+ // ── Compression (gzip / brotli) ──────────────────────────────────
124
+
125
+ macro_rules! compress_to_out {
126
+ ($name:ident, $core:path, $extra:ty) => {
127
+ #[doc = concat!("Run `", stringify!($core), "` and write the result into `out`.")]
128
+ ///
129
+ /// # Convention
130
+ /// Returns the exact total bytes the result requires (the written count
131
+ /// on success, or the needed size when `out_cap` is too small). The JS
132
+ /// wrapper compares against `out_cap` to distinguish success from
133
+ /// too-small, so a miss allocates EXACTLY once and retries — it never
134
+ /// re-runs the whole (de)compression in a grow loop. `0` is reserved
135
+ /// for a REAL error (invalid stream / decompression-cap exceeded).
136
+ ///
137
+ /// # Safety
138
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
139
+ #[no_mangle]
140
+ pub unsafe extern "C" fn $name(
141
+ data: *const u8,
142
+ len: usize,
143
+ extra: $extra,
144
+ out: *mut u8,
145
+ out_cap: usize,
146
+ ) -> usize {
147
+ if data.is_null() || out.is_null() {
148
+ return 0;
149
+ }
150
+ let input = slice::from_raw_parts(data, len);
151
+ let output = slice::from_raw_parts_mut(out, out_cap);
152
+ panic_guard(
153
+ || match $core(input, extra, output) {
154
+ Ok(w) => w,
155
+ // Needed-size convention: report the EXACT required size so the
156
+ // JS wrapper allocates once and retries (never a re-run loop).
157
+ Err(crate::payload::compress::StreamError::TooSmall { needed }) => needed,
158
+ // Real error (invalid stream / decompression cap exceeded) → 0.
159
+ Err(_) => 0,
160
+ },
161
+ 0,
162
+ )
163
+ }
164
+ };
165
+ }
166
+
167
+ compress_to_out!(
168
+ castrum_gzip_compress,
169
+ crate::payload::compress::gzip_compress_into,
170
+ u32
171
+ );
172
+ compress_to_out!(
173
+ castrum_gzip_decompress,
174
+ crate::payload::compress::gzip_decompress_into,
175
+ usize
176
+ );
177
+ compress_to_out!(
178
+ castrum_brotli_compress,
179
+ crate::payload::compress::brotli_compress_into,
180
+ u32
181
+ );
182
+ compress_to_out!(
183
+ castrum_brotli_decompress,
184
+ crate::payload::compress::brotli_decompress_into,
185
+ usize
186
+ );
187
+
188
+ /// Read the original (uncompressed) size from a gzip stream's ISIZE trailer
189
+ /// (last 4 bytes, little-endian, original size mod 2^32). Returns the EXACT
190
+ /// output size for a single-member standard gzip stream, or 0 when the input
191
+ /// isn't a usable standard gzip stream (too short / not gzip magic). Lets the
192
+ /// JS wrapper pre-size the decompress buffer exactly so the happy path is a
193
+ /// single pass (no grow-retry re-run).
194
+ ///
195
+ /// # Safety
196
+ /// `data` must be valid for reads of `len` bytes.
197
+ #[no_mangle]
198
+ pub unsafe extern "C" fn castrum_gzip_isize(data: *const u8, len: usize) -> u32 {
199
+ if data.is_null() || len < 18 {
200
+ return 0;
201
+ }
202
+ let s = slice::from_raw_parts(data, len);
203
+ if s[0] != 0x1f || s[1] != 0x8b {
204
+ return 0;
205
+ }
206
+ u32::from_le_bytes([s[len - 4], s[len - 3], s[len - 2], s[len - 1]])
207
+ }
208
+
209
+ /// Encode one SSE event into `out`. Optional fields use `flags` bits
210
+ /// (1 = event present, 2 = id present, 4 = retry present) so a present-but-empty
211
+ /// string is distinct from an absent one, matching the napi `Option<String>`.
212
+ /// Returns bytes written, or 0 on a too-small buffer / invalid UTF-8 in
213
+ /// event/id (JS sizes `data_len + 64` to guarantee success). FFI sibling of the
214
+ /// napi `sse_encode_event` — the wrapper decodes nothing (output is raw SSE
215
+ /// bytes), so this just removes the napi crossing.
216
+ ///
217
+ /// # Safety
218
+ /// `event`/`id` must be valid for reads of `event_len`/`id_len` bytes when their
219
+ /// flag is set; `data` valid for `data_len` reads; `out` for `out_cap` writes.
220
+ #[no_mangle]
221
+ pub unsafe extern "C" fn castrum_sse_encode_into(
222
+ event: *const u8,
223
+ event_len: usize,
224
+ data: *const u8,
225
+ data_len: usize,
226
+ id: *const u8,
227
+ id_len: usize,
228
+ flags: u8,
229
+ retry: u32,
230
+ out: *mut u8,
231
+ out_cap: usize,
232
+ ) -> usize {
233
+ if data.is_null() || out.is_null() {
234
+ return 0;
235
+ }
236
+ // A PRESENT but empty event/id is valid (emits the line) — only a null
237
+ // pointer with its flag set is malformed (would be UB to slice).
238
+ if (flags & 1 != 0 && event.is_null()) || (flags & 2 != 0 && id.is_null()) {
239
+ return 0;
240
+ }
241
+ panic_guard(
242
+ || -> Option<usize> {
243
+ let data_bytes = slice::from_raw_parts(data, data_len);
244
+ let output = slice::from_raw_parts_mut(out, out_cap);
245
+ let event_opt = if flags & 1 != 0 {
246
+ Some(std::str::from_utf8(slice::from_raw_parts(event, event_len)).ok()?)
247
+ } else {
248
+ None
249
+ };
250
+ let id_opt = if flags & 2 != 0 {
251
+ Some(std::str::from_utf8(slice::from_raw_parts(id, id_len)).ok()?)
252
+ } else {
253
+ None
254
+ };
255
+ let retry_opt = if flags & 4 != 0 {
256
+ Some(u64::from(retry))
257
+ } else {
258
+ None
259
+ };
260
+ match crate::payload::sse::encode_event_into_slice(
261
+ event_opt, data_bytes, id_opt, retry_opt, output,
262
+ ) {
263
+ Ok(w) => Some(w),
264
+ // Too-small buffer: return the exact required size so the JS
265
+ // wrapper allocates ONCE and retries (needed-size convention);
266
+ // 0 stays a real error (invalid UTF-8 in event/id above).
267
+ Err(_) => Some(crate::payload::sse::encode_event_size(
268
+ event_opt, data_bytes, id_opt, retry_opt,
269
+ )),
270
+ }
271
+ },
272
+ None,
273
+ )
274
+ .unwrap_or(0)
275
+ }
@@ -0,0 +1,60 @@
1
+ // rust/ffi/probe.rs — diagnostic C-ABI probes (bench-only, NOT in the ffi.ts surface).
2
+ //
3
+ // Used ONLY by bench/ffi-margin.ts to isolate the fixed per-call FFI cost into
4
+ // its components: the bare trampoline (noop), scalar-arg/return conversion
5
+ // (echo_usize, which can be bound with either `usize` or `u64_fast` to measure
6
+ // BigInt boxing in isolation), TypedArray-view→pointer resolution
7
+ // (echo_view), and `cstring`-ARG transcoding (echo_cstr — the engine encodes
8
+ // the JS string to a call-scoped NUL-terminated buffer; the callee borrows it
9
+ // via `CStr::from_ptr`, never dereferencing beyond NUL). They are trivial,
10
+ // non-fallible, allocate nothing, and are NOT part of the shipped
11
+ // `src/native/ffi.ts` dlopen map, so the bind-time self-test does not cover
12
+ // them. The `ffi_probe_*` prefix (NOT `castrum_*`) keeps them out of the
13
+ // shipped-symbol namespace so the source-level symbol-parity guard
14
+ // (test/unit/features/ffi-symbol-parity.test.ts) stays meaningful.
15
+
16
+ use std::slice;
17
+
18
+ /// Bare C-ABI trampoline floor: does nothing, returns nothing.
19
+ #[no_mangle]
20
+ pub extern "C" fn ffi_probe_noop() {}
21
+
22
+ /// Scalar pass-through: returns `v` unchanged. Measures scalar-arg + return
23
+ /// conversion only (bind with `usize` vs `u64_fast` to isolate BigInt boxing).
24
+ #[no_mangle]
25
+ pub extern "C" fn ffi_probe_echo_usize(v: usize) -> usize {
26
+ v
27
+ }
28
+
29
+ /// View pass-through: returns the byte length of the `(ptr, len)` pair.
30
+ /// Measures TypedArray-view→pointer resolution + (ptr,len) arg conversion.
31
+ ///
32
+ /// # Safety
33
+ /// `data` must be valid for reads of `len` bytes (the pointer is only used to
34
+ /// form a slice whose length we return — never dereferenced).
35
+ #[no_mangle]
36
+ pub unsafe extern "C" fn ffi_probe_echo_view(data: *const u8, len: usize) -> usize {
37
+ if data.is_null() && len != 0 {
38
+ return 0;
39
+ }
40
+ let _ = slice::from_raw_parts(data, len);
41
+ len
42
+ }
43
+
44
+ /// `cstring`-ARG pass-through: returns the byte length of the NUL-terminated
45
+ /// C string the engine produced by transcoding a JS string arg. Measures the
46
+ /// engine-side JS-string→UTF-8 transcode + call-scoped buffer + callee
47
+ /// `CStr::from_ptr` borrow — the cost a `'cstring'` input arg adds compared
48
+ /// with a JS-side `encoder.encode` + `(ptr,len)` pair. The pointer is only
49
+ /// used to form a slice whose length we return — never dereferenced beyond
50
+ /// NUL.
51
+ ///
52
+ /// # Safety
53
+ /// `data` must be a valid NUL-terminated C string for reads up to its terminator.
54
+ #[no_mangle]
55
+ pub unsafe extern "C" fn ffi_probe_echo_cstr(data: *const std::os::raw::c_char) -> usize {
56
+ if data.is_null() {
57
+ return 0;
58
+ }
59
+ std::ffi::CStr::from_ptr(data).to_bytes().len()
60
+ }
@@ -0,0 +1,77 @@
1
+ // rust/ffi/rate_limit.rs — rate-limiter C-ABI exports.
2
+ //
3
+ // Both entries check a limit via the opaque `RateLimiter` handle; one hashes a
4
+ // `cstring` key in-engine, the other takes a pre-hashed i64 key. The packed
5
+ // verdict is written by the shared `write_rate_check` helper in `util.rs`.
6
+
7
+ use super::util::{panic_guard, write_rate_check};
8
+
9
+ /// RateLimiter: check a rate limit for a STRING key (hashed internally) at
10
+ /// `now_ms` via its opaque inner handle. Writes packed `[u8 allowed][u32
11
+ /// remaining LE][i64 reset_ms LE]` (13 bytes); needed-size convention; 0 =
12
+ /// null handle (real error).
13
+ ///
14
+ /// `key` is a `bun:ffi` `cstring` ARG — the engine transcodes the JS rate-limit
15
+ /// key in-engine (JS does zero encode; the callee borrows via `CStr::from_ptr`).
16
+ /// Intended keys are IPs / route identifiers (text, no NUL); a key containing a
17
+ /// NUL would be truncated before hashing.
18
+ ///
19
+ /// # Safety
20
+ /// `inner` must be a valid `RateLimiter` pointer from `inner_ptr()`, alive for
21
+ /// the call; `key` must be a valid NUL-terminated C string; `out` for writes up
22
+ /// to `out_cap`.
23
+ #[no_mangle]
24
+ pub unsafe extern "C" fn castrum_rate_limiter_check(
25
+ inner: usize,
26
+ key: *const std::os::raw::c_char,
27
+ now_ms: i64,
28
+ out: *mut u8,
29
+ out_cap: usize,
30
+ ) -> usize {
31
+ if inner == 0 || key.is_null() || out.is_null() {
32
+ return 0;
33
+ }
34
+ let k = std::ffi::CStr::from_ptr(key).to_bytes();
35
+ let hashed = crate::crypto::hashing::fast_hash_bytes(k);
36
+ let (allowed, remaining, reset_ms) = panic_guard(
37
+ || unsafe {
38
+ crate::ingress::rate_limit::rate_limiter_check_core(
39
+ inner as *const crate::ingress::rate_limit::RateLimiter,
40
+ hashed,
41
+ now_ms as u64,
42
+ )
43
+ },
44
+ (false, 0, 0),
45
+ );
46
+ write_rate_check(allowed, remaining, reset_ms, out, out_cap)
47
+ }
48
+
49
+ /// RateLimiter: check a rate limit for a PRE-HASHED i64 key at `now_ms` via its
50
+ /// opaque inner handle (packed `[u8 allowed][u32 remaining LE][i64 reset_ms LE]`).
51
+ ///
52
+ /// # Safety
53
+ /// `inner` must be a valid `RateLimiter` pointer from `inner_ptr()`, alive for
54
+ /// the call; `out` for writes up to `out_cap`.
55
+ #[no_mangle]
56
+ pub unsafe extern "C" fn castrum_rate_limiter_check_key(
57
+ inner: usize,
58
+ key: i64,
59
+ now_ms: i64,
60
+ out: *mut u8,
61
+ out_cap: usize,
62
+ ) -> usize {
63
+ if inner == 0 || out.is_null() {
64
+ return 0;
65
+ }
66
+ let (allowed, remaining, reset_ms) = panic_guard(
67
+ || unsafe {
68
+ crate::ingress::rate_limit::rate_limiter_check_core(
69
+ inner as *const crate::ingress::rate_limit::RateLimiter,
70
+ key as u64,
71
+ now_ms as u64,
72
+ )
73
+ },
74
+ (false, 0, 0),
75
+ );
76
+ write_rate_check(allowed, remaining, reset_ms, out, out_cap)
77
+ }
@@ -0,0 +1,85 @@
1
+ // rust/ffi/route.rs — per-route native stack C-ABI exports (`castrum_route_*`).
2
+ //
3
+ // The live external wire consumed by `@ignex/native` (`createNativeRoute` →
4
+ // `route-wire.ts` v3): a route descriptor compiles ONCE into a pre-baked
5
+ // `NativeRoute` (rust/ingress/native_route.rs), then each request packs a tiny
6
+ // frame and gets a packed verdict in one call. The handle is an owned
7
+ // `Box<NativeRoute>` (opaque u64); `route_run` follows the needed-size
8
+ // convention (`0` = real error, `> out_cap` = exact required size) so the JS
9
+ // wrapper allocates once and retries at most once. All three are `panic_guard`ed
10
+ // — a panic must never unwind through the C ABI.
11
+
12
+ use std::slice;
13
+
14
+ use super::util::panic_guard;
15
+
16
+ /// Compile a route descriptor into an opaque handle (`0` = failure / null
17
+ /// input / panic). The handle is an owned `Box<NativeRoute>`; release it with
18
+ /// {@link castrum_route_destroy}.
19
+ ///
20
+ /// # Safety
21
+ /// `desc` must be valid for reads of `desc_len` bytes.
22
+ #[no_mangle]
23
+ pub unsafe extern "C" fn castrum_route_compile(desc: *const u8, desc_len: usize) -> u64 {
24
+ if desc.is_null() {
25
+ return 0;
26
+ }
27
+ let bytes = slice::from_raw_parts(desc, desc_len);
28
+ panic_guard(
29
+ || match crate::ingress::NativeRoute::compile(bytes) {
30
+ Ok(route) => Box::into_raw(Box::new(route)) as u64,
31
+ Err(_) => 0,
32
+ },
33
+ 0,
34
+ )
35
+ }
36
+
37
+ /// Run a compiled route on one request frame, writing the packed verdict into
38
+ /// `out`. Returns bytes written (`0` = real error / malformed frame / panic;
39
+ /// `> out_cap` = the EXACT required size — allocate once and retry).
40
+ ///
41
+ /// # Safety
42
+ /// `handle` must be a live handle from {@link castrum_route_compile} (not yet
43
+ /// destroyed); `frame` valid for `frame_len` reads; `out` valid for `out_cap`
44
+ /// writes. The route is immutable (`&self`, no interior mutability), so
45
+ /// concurrent calls from multiple worker threads are safe.
46
+ #[no_mangle]
47
+ pub unsafe extern "C" fn castrum_route_run(
48
+ handle: u64,
49
+ frame: *const u8,
50
+ frame_len: usize,
51
+ out: *mut u8,
52
+ out_cap: usize,
53
+ ) -> usize {
54
+ if handle == 0 || frame.is_null() || out.is_null() {
55
+ return 0;
56
+ }
57
+ let route: &crate::ingress::NativeRoute = &*(handle as *const crate::ingress::NativeRoute);
58
+ let frame_slice = slice::from_raw_parts(frame, frame_len);
59
+ let out_slice = slice::from_raw_parts_mut(out, out_cap);
60
+ // Aliasing insurance (mirrors castrum_ingress_handle_packed): a pooled
61
+ // frame that overlaps `out` must be copied before the shared &/&mut borrow
62
+ // pair (aliased &/&mut is instant UB). The JS wrapper uses distinct pooled
63
+ // buffers, so this is a rare defensive copy.
64
+ let owned_frame;
65
+ let frame_ref: &[u8] = if crate::util::slices_overlap(frame_slice, out_slice) {
66
+ owned_frame = frame_slice.to_vec();
67
+ &owned_frame
68
+ } else {
69
+ frame_slice
70
+ };
71
+ panic_guard(|| route.run(frame_ref, out_slice).unwrap_or(0), 0)
72
+ }
73
+
74
+ /// Destroy a compiled route handle (frees the `Box<NativeRoute>`). A null
75
+ /// handle is a no-op; double-destroy is UB and must not be called.
76
+ ///
77
+ /// # Safety
78
+ /// `handle` must come from {@link castrum_route_compile} and be destroyed at
79
+ /// most once.
80
+ #[no_mangle]
81
+ pub unsafe extern "C" fn castrum_route_destroy(handle: u64) {
82
+ if handle != 0 {
83
+ drop(Box::from_raw(handle as *mut crate::ingress::NativeRoute));
84
+ }
85
+ }
@@ -0,0 +1,172 @@
1
+ // rust/ffi/task.rs — C-ABI exports for the off-thread task runtime.
2
+ //
3
+ // Eight symbols, all thin: the pool/ring/ops live in `rust/task/`. `submit`
4
+ // COPIES the packed args onto the worker (the JS caller's pooled buffer is
5
+ // released as soon as submit returns), and `drain` follows the repo-wide
6
+ // needed-size convention so JS grows once and retries once.
7
+
8
+ use std::slice;
9
+
10
+ use super::util::panic_guard;
11
+ use crate::task;
12
+
13
+ /// Start the pool with `threads` workers (0 → runtime default).
14
+ /// Returns `0` started, `1` already running, `2` internal error.
15
+ #[no_mangle]
16
+ pub extern "C" fn castrum_task_init(threads: u32) -> u32 {
17
+ panic_guard(|| task::init(Some(threads)), 2)
18
+ }
19
+
20
+ /// Submit `op` with `args_len` packed arg bytes, completing under `task_id`.
21
+ /// Returns `1` accepted, `0` rejected (null args with a non-zero length).
22
+ ///
23
+ /// # Safety
24
+ /// `args` must be valid for reads of `args_len` bytes.
25
+ #[no_mangle]
26
+ pub unsafe extern "C" fn castrum_task_submit(
27
+ op: u32,
28
+ args: *const u8,
29
+ args_len: usize,
30
+ task_id: usize,
31
+ ) -> u32 {
32
+ if args.is_null() && args_len != 0 {
33
+ return 0;
34
+ }
35
+ let owned: Vec<u8> = if args_len == 0 {
36
+ Vec::new()
37
+ } else {
38
+ slice::from_raw_parts(args, args_len).to_vec()
39
+ };
40
+ u32::from(task::submit_op(op, owned, task_id as u64))
41
+ }
42
+
43
+ /// Drain finished tasks into `out` (packed `[u32 count][entry…]`).
44
+ /// Returns bytes written, `0` when nothing is pending, or the EXACT required
45
+ /// size when `out` is too small (nothing is consumed in that case).
46
+ ///
47
+ /// # Safety
48
+ /// `out` must be valid for writes of `out_cap` bytes.
49
+ #[no_mangle]
50
+ pub unsafe extern "C" fn castrum_task_drain(out: *mut u8, out_cap: usize) -> usize {
51
+ if out.is_null() {
52
+ return 0;
53
+ }
54
+ panic_guard(|| task::drain(slice::from_raw_parts_mut(out, out_cap)), 0)
55
+ }
56
+
57
+ /// Completions waiting to be drained.
58
+ #[no_mangle]
59
+ pub extern "C" fn castrum_task_pending() -> u32 {
60
+ task::pending()
61
+ }
62
+
63
+ /// Submit an op that writes its result DIRECTLY into `out` (zero-copy output):
64
+ /// the completion body is the 8-byte LE written length, not the data, so JS
65
+ /// resolves a view of the buffer it already owns. A too-small `out` completes
66
+ /// with status 3 (TOO_SMALL) and the exact required size as the body; status 4
67
+ /// (UNSUPPORTED) means the op has no `_into` form and the caller should fall
68
+ /// back to `castrum_task_submit`.
69
+ ///
70
+ /// Returns `1` accepted, `0` rejected.
71
+ ///
72
+ /// # Safety
73
+ /// `args` must be valid for reads of `args_len` bytes. `out` must be valid for
74
+ /// writes of `out_cap` bytes and must stay alive (and untouched by JS) until the
75
+ /// completion for `task_id` is drained.
76
+ #[no_mangle]
77
+ pub unsafe extern "C" fn castrum_task_submit_out(
78
+ op: u32,
79
+ args: *const u8,
80
+ args_len: usize,
81
+ task_id: usize,
82
+ out: *mut u8,
83
+ out_cap: usize,
84
+ ) -> u32 {
85
+ if (args.is_null() && args_len != 0) || out.is_null() || out_cap == 0 {
86
+ return 0;
87
+ }
88
+ let owned: Vec<u8> = if args_len == 0 {
89
+ Vec::new()
90
+ } else {
91
+ slice::from_raw_parts(args, args_len).to_vec()
92
+ };
93
+ u32::from(task::submit_op_into(
94
+ op,
95
+ owned,
96
+ task_id as u64,
97
+ out as usize,
98
+ out_cap,
99
+ ))
100
+ }
101
+
102
+ /// Mark `task_id` cancelled. Returns `1` when newly marked, `0` otherwise.
103
+ #[no_mangle]
104
+ pub extern "C" fn castrum_task_cancel(task_id: usize) -> u32 {
105
+ u32::from(task::mark_cancelled(task_id as u64))
106
+ }
107
+
108
+ /// Submit an op whose PAYLOAD is read IN PLACE from the caller's buffer
109
+ /// (zero-copy INPUT). Only the `hdr_len` header bytes are copied, so a large
110
+ /// argument never crosses the boundary — without this, offloading a big
111
+ /// compression costs a full-size copy on the JS thread and measured WORSE than
112
+ /// the synchronous built-in (docs/RND-CONCURRENCY.md §7b).
113
+ ///
114
+ /// Returns `1` accepted, `0` rejected.
115
+ ///
116
+ /// # Safety
117
+ /// `hdr` must be valid for reads of `hdr_len` bytes. `data` must be valid for
118
+ /// reads of `data_len` bytes and must stay alive (and unmodified by JS) until
119
+ /// the completion for `task_id` is drained.
120
+ #[no_mangle]
121
+ pub unsafe extern "C" fn castrum_task_submit_slice(
122
+ op: u32,
123
+ hdr: *const u8,
124
+ hdr_len: usize,
125
+ data: *const u8,
126
+ data_len: usize,
127
+ task_id: usize,
128
+ ) -> u32 {
129
+ if (hdr.is_null() && hdr_len != 0) || (data.is_null() && data_len != 0) {
130
+ return 0;
131
+ }
132
+ // The header is small, so copying it keeps the caller free to reuse a
133
+ // scratch buffer; the payload is never copied.
134
+ let owned = if hdr_len == 0 {
135
+ Vec::new()
136
+ } else {
137
+ slice::from_raw_parts(hdr, hdr_len).to_vec()
138
+ };
139
+ u32::from(task::submit_slice_op(
140
+ op,
141
+ owned,
142
+ data as usize,
143
+ data_len,
144
+ task_id as u64,
145
+ ))
146
+ }
147
+
148
+ /// Register the thread-safe doorbell trampoline address (`0` disables it).
149
+ /// Returns `1` on success.
150
+ ///
151
+ /// # Safety
152
+ /// `cb` must be a live `void()` C trampoline that is safe to call from any
153
+ /// thread (a `bun:ffi` `JSCallback({ threadsafe: true }).ptr`).
154
+ #[no_mangle]
155
+ pub extern "C" fn castrum_task_set_doorbell(cb: *const std::os::raw::c_void) -> u32 {
156
+ task::set_doorbell(cb as usize);
157
+ 1
158
+ }
159
+
160
+ /// Stop the pool and drop pending completions. Returns `0`.
161
+ #[no_mangle]
162
+ pub extern "C" fn castrum_task_shutdown() -> u32 {
163
+ task::shutdown();
164
+ task::completion::clear();
165
+ 0
166
+ }
167
+
168
+ /// Configured pool worker count (0 before the pool starts).
169
+ #[no_mangle]
170
+ pub extern "C" fn castrum_task_threads() -> u32 {
171
+ task::threads()
172
+ }