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,495 @@
1
+ // rust/src/bin/native-bench.rs — native-only timing of the pure-Rust cores.
2
+ //
3
+ // Diagnostic binary (NEVER shipped; not part of the addon). It times the same
4
+ // core functions `bun:ffi` wraps — but with NO JS / FFI boundary — so
5
+ // `bench/ffi-margin.ts` can compare them against the same ops called through
6
+ // `bun:ffi` and pinpoint exactly where the crossing cost is.
7
+ //
8
+ // Inputs: reads base64-NDJSON on stdin (`key\t<base64>`), byte-identical to
9
+ // what the JS runner feeds its FFI side, so both sides measure the same work.
10
+ // Missing keys fall back to the embedded defaults below (so
11
+ // `cargo run --release --bin native-bench` works standalone).
12
+ //
13
+ // Output: one `name\t<ns_per_op>` line per op (min-of-3 batches, after
14
+ // warmup). Build/run from the repo root:
15
+ // cargo run --release --bin native-bench < inputs.ndjson
16
+
17
+ use std::collections::HashMap;
18
+ use std::hint::black_box;
19
+ use std::io::{self, BufRead};
20
+ use std::time::Instant;
21
+
22
+ use aws_lc_rs::aead::AES_256_GCM;
23
+ use aws_lc_rs::hmac;
24
+ use base64::engine::general_purpose::STANDARD as B64;
25
+ use base64::Engine as _;
26
+
27
+ use castrum::crypto::base64::{
28
+ base64_decode_into_slice, base64_encode_into_slice, hex_decode_into_slice,
29
+ hex_encode_into_slice,
30
+ };
31
+ use castrum::crypto::hashing::{crc32_bytes, fast_hash_bytes, fnv1a64_bytes};
32
+ use castrum::http::cookie_parser::cookie_parse_packed_into_slice;
33
+ use castrum::http::etag::etag_from_crc32_into;
34
+ use castrum::http::http_date::http_date_into_slice;
35
+ use castrum::http::http_parser::http_parse_request_packed_into_slice;
36
+ use castrum::http::query_parser::query_parse_packed_into_slice;
37
+ use castrum::http::url_codec::{url_decode_into_slice, url_encode_into_slice};
38
+ use castrum::json::json_ops::{json_sum_ids_bytes, json_valid_bytes};
39
+ use castrum::payload::compress::{
40
+ brotli_compress_into, brotli_decompress_into, gzip_compress_into, gzip_decompress_into,
41
+ };
42
+ use castrum::payload::sse::encode_event_into_slice;
43
+ use castrum::payload::websocket::ws_accept_key_into;
44
+ use castrum::util::validation::{
45
+ validate_email_bytes, validate_ipv4_bytes, validate_ipv6_bytes, validate_uuid_bytes,
46
+ };
47
+
48
+ // ── Embedded default inputs (used when stdin supplies no key) ──────────────
49
+ const DEFAULT_CRC: &[u8] = b"Hello, practical CRC32 checksum test data!";
50
+ const DEFAULT_ENCODE: &[u8] = b"the quick brown fox jumps over the lazy dog 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789";
51
+ const DEFAULT_HEX: &[u8] = b"74686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f6720303132333435363738392030313233343536373839203031323334353637383920303132333435363738392030313233343536373839203031323334353637383920303132333435363738392030313233343536373839";
52
+ const DEFAULT_URL_ENCODE: &[u8] = b"hello world & foo=bar";
53
+ const DEFAULT_URL_DECODE: &[u8] = b"hello%20world%20%26%20foo%3Dbar";
54
+ const DEFAULT_EMAIL: &[u8] = b"user@example.com";
55
+ const DEFAULT_UUID: &[u8] = b"550e8400-e29b-41d4-a716-446655440000";
56
+ const DEFAULT_IPV4: &[u8] = b"192.168.1.100";
57
+ const DEFAULT_IPV6: &[u8] = b"2001:db8::1";
58
+ const DEFAULT_JSON: &[u8] = b"[{\"id\":1,\"name\":\"a\"},{\"id\":2,\"name\":\"b\"}]";
59
+ const DEFAULT_ETAG: &[u8] = b"etag data payload for the margin bench";
60
+ const DEFAULT_WS_KEY: &[u8] = b"dGhlIHNhbXBsZSBub25jZQ==";
61
+ const DEFAULT_HTTP: &[u8] = b"GET /api/users?page=1&limit=20 HTTP/1.1\r\nHost: example.com\r\nAccept: application/json\r\n\r\n";
62
+ const DEFAULT_QUERY: &[u8] = b"name=John+Doe&age=30&tags[]=a&tags[]=b";
63
+ const DEFAULT_COOKIE: &[u8] = b"session=abc123; theme=dark; lang=en-US";
64
+ const DEFAULT_HMAC_KEY: &[u8] = b"super-secret-key-2026";
65
+ const DEFAULT_HMAC_DATA: &[u8] = b"message to sign with HMAC-SHA256";
66
+ const DEFAULT_COMPRESS: &[u8] = b"row 0: the quick brown fox jumps over the lazy dog 0\nrow 1: the quick brown fox jumps over the lazy dog 1\nrow 2: the quick brown fox jumps over the lazy dog 2";
67
+ const DEFAULT_COOKIE_VALUE: &[u8] = b"session-value";
68
+ const DEFAULT_COOKIE_SECRET: &[u8] = b"s3cr3t-secret";
69
+ const DEFAULT_CSRF_SECRET: &[u8] = b"csrf-secret-2026";
70
+ const DEFAULT_PASSWORD: &[u8] = b"correct horse battery staple";
71
+ const DEFAULT_PASSWORD_SALT: &[u8] = b"0123456789abcdef";
72
+ const DEFAULT_PBKDF2_PW: &[u8] = b"password";
73
+ const DEFAULT_PBKDF2_SALT: &[u8] = b"salt";
74
+ const DEFAULT_AEAD_KEY: &[u8] = b"0123456789abcdef0123456789abcdef";
75
+ const DEFAULT_AEAD_NONCE: &[u8] = b"0123456789ab";
76
+ const DEFAULT_AEAD_PT: &[u8] = b"sensitive session payload for the margin bench";
77
+ const DEFAULT_WS_PAYLOAD: &[u8] = b"Hello WebSocket! x10";
78
+ const DEFAULT_JSON_DOC: &[u8] = b"{\"a\":\"b\",\"c\":1}";
79
+ const DEFAULT_JSON_PATCH: &[u8] = b"[{\"op\":\"replace\",\"path\":\"/a\",\"value\":42}]";
80
+ const DEFAULT_JWT_CLAIMS: &[u8] = b"{\"sub\":\"user-1\"}";
81
+ const DEFAULT_JWT_SECRET: &[u8] = b"my-secret";
82
+ const DEFAULT_MULTIPART_BODY: &[u8] = b"--FormBoundary1234\r\nContent-Disposition: form-data; name=\"field1\"\r\n\r\nhello world\r\n--FormBoundary1234--\r\n";
83
+ const DEFAULT_MULTIPART_BOUNDARY: &[u8] = b"FormBoundary1234";
84
+ const DEFAULT_HTTP_DATE_SECS: i64 = 1_750_000_000;
85
+
86
+ /// Min-of-3-batches timing: warm up, then take the fastest batch's ns/op.
87
+ fn bench<F: FnMut() -> u64>(name: &str, iters: u64, mut f: F) {
88
+ for _ in 0..(iters / 20).max(1) {
89
+ black_box(f());
90
+ }
91
+ let mut best = u64::MAX;
92
+ for _ in 0..3 {
93
+ let start = Instant::now();
94
+ for _ in 0..iters {
95
+ black_box(f());
96
+ }
97
+ let ns = start.elapsed().as_nanos() as u64;
98
+ if ns < best {
99
+ best = ns;
100
+ }
101
+ }
102
+ println!("{}\t{:.1}", name, best as f64 / iters as f64);
103
+ }
104
+
105
+ fn packed_out_cap(input_len: usize) -> usize {
106
+ input_len * 9 + 16
107
+ }
108
+
109
+ fn main() {
110
+ let mut inputs: HashMap<String, Vec<u8>> = HashMap::new();
111
+ let stdin = io::stdin();
112
+ for line in stdin.lock().lines().map_while(Result::ok) {
113
+ let line = line.trim_end();
114
+ if line.is_empty() {
115
+ continue;
116
+ }
117
+ if let Some((key, b64)) = line.split_once('\t') {
118
+ if let Ok(bytes) = B64.decode(b64) {
119
+ inputs.insert(key.to_string(), bytes);
120
+ }
121
+ }
122
+ }
123
+ let inp = |key: &str, fallback: &[u8]| -> Vec<u8> {
124
+ inputs
125
+ .get(key)
126
+ .cloned()
127
+ .unwrap_or_else(|| fallback.to_vec())
128
+ };
129
+
130
+ let crc = inp("crc_input", DEFAULT_CRC);
131
+ let encode = inp("encode_data", DEFAULT_ENCODE);
132
+ let hex_in = inp("hex_input", DEFAULT_HEX);
133
+ let url_enc = inp("url_encode_input", DEFAULT_URL_ENCODE);
134
+ let url_dec = inp("url_decode_input", DEFAULT_URL_DECODE);
135
+ let email = inp("email_ok", DEFAULT_EMAIL);
136
+ let uuid = inp("uuid_ok", DEFAULT_UUID);
137
+ let ipv4 = inp("ipv4_ok", DEFAULT_IPV4);
138
+ let ipv6 = inp("ipv6_ok", DEFAULT_IPV6);
139
+ let json = inp("json_payload", DEFAULT_JSON);
140
+ let etag_data = inp("etag_data", DEFAULT_ETAG);
141
+ let ws_key = inp("ws_key", DEFAULT_WS_KEY);
142
+ let http = inp("http_raw", DEFAULT_HTTP);
143
+ let query = inp("query_str", DEFAULT_QUERY);
144
+ let cookie = inp("cookie_str", DEFAULT_COOKIE);
145
+ let hmac_key = inp("hmac_key", DEFAULT_HMAC_KEY);
146
+ let hmac_data = inp("hmac_data", DEFAULT_HMAC_DATA);
147
+ let compress = inp("compress_payload", DEFAULT_COMPRESS);
148
+ let base64_input = inp("base64_input", B64.encode(&encode).as_bytes());
149
+ let hmac_sig = inp("hmac_sig", &[0u8; 64]);
150
+ let cookie_value = inp("cookie_value", DEFAULT_COOKIE_VALUE);
151
+ let cookie_secret = inp("cookie_secret", DEFAULT_COOKIE_SECRET);
152
+ let csrf_secret = inp("csrf_secret", DEFAULT_CSRF_SECRET);
153
+ let password = inp("password_bytes", DEFAULT_PASSWORD);
154
+ let password_salt = inp("password_salt", DEFAULT_PASSWORD_SALT);
155
+ let pbkdf2_pw = inp("pbkdf2_password", DEFAULT_PBKDF2_PW);
156
+ let pbkdf2_salt = inp("pbkdf2_salt", DEFAULT_PBKDF2_SALT);
157
+ let aead_key = inp("aead_key", DEFAULT_AEAD_KEY);
158
+ let aead_nonce = inp("aead_nonce", DEFAULT_AEAD_NONCE);
159
+ let aead_pt = inp("aead_plaintext", DEFAULT_AEAD_PT);
160
+ let ws_payload = inp("ws_payload", DEFAULT_WS_PAYLOAD);
161
+ let json_doc = inp("json_doc", DEFAULT_JSON_DOC);
162
+ let json_patch_data = inp("json_patch_data", DEFAULT_JSON_PATCH);
163
+ let jwt_claims = inp("jwt_claims", DEFAULT_JWT_CLAIMS);
164
+ let jwt_secret = inp("jwt_secret", DEFAULT_JWT_SECRET);
165
+ let multipart_body = inp("multipart_body", DEFAULT_MULTIPART_BODY);
166
+ let multipart_boundary = inp("multipart_boundary", DEFAULT_MULTIPART_BOUNDARY);
167
+ let gzip_compressed = inp("gzip_compressed", &[]);
168
+ let brotli_compressed = inp("brotli_compressed", &[]);
169
+ let content_type = inp(
170
+ "content_type_multipart",
171
+ b"multipart/form-data; boundary=FormBoundary1234; charset=UTF-8".as_slice(),
172
+ );
173
+
174
+ // Reusable output buffers.
175
+ let mut hex_out = vec![0u8; encode.len() * 2];
176
+ let mut hex_dec_out = vec![0u8; hex_in.len() / 2 + 1];
177
+ let mut b64_out = vec![0u8; encode.len() * 4 / 3 + 4];
178
+ let mut b64_dec_out = vec![0u8; encode.len() + 8];
179
+ let mut url_out = vec![0u8; url_enc.len() * 3];
180
+ let mut urld_out = vec![0u8; url_dec.len()];
181
+ let mut etag_out = vec![0u8; 32];
182
+ let mut date_out = vec![0u8; 32];
183
+ let mut ws_out = vec![0u8; 32];
184
+ let mut http_out = vec![0u8; packed_out_cap(http.len())];
185
+ let mut query_out = vec![0u8; packed_out_cap(query.len())];
186
+ let mut cookie_out = vec![0u8; packed_out_cap(cookie.len())];
187
+ let mut gz_out = vec![0u8; compress.len() * 2 + 64];
188
+ let mut br_out = vec![0u8; compress.len() * 2 + 64];
189
+ let mut gz_d_out = vec![0u8; compress.len() * 2 + 64];
190
+ let mut br_d_out = vec![0u8; compress.len() * 2 + 64];
191
+ let mut cookie_sig_out = vec![0u8; cookie_value.len() + 65];
192
+ let mut cookie_val_out = vec![0u8; cookie_value.len() + 65];
193
+ let mut csrf_out = [0u8; 129];
194
+ let mut ws_frame_out = vec![0u8; ws_payload.len() + 14];
195
+ let mut pbkdf2_out = vec![0u8; 32];
196
+ let mut random_out = vec![0u8; 32];
197
+
198
+ // Compiled once (matches the C-ABI per-thread key cache steady state).
199
+ let hmac_key_compiled = hmac::Key::new(hmac::HMAC_SHA256, &hmac_key);
200
+ let cookie_key = hmac::Key::new(hmac::HMAC_SHA256, &cookie_secret);
201
+ let csrf_key = hmac::Key::new(hmac::HMAC_SHA256, &csrf_secret);
202
+ let mut hmac_tag_out = [0u8; 64];
203
+ let mut sig_bytes = [0u8; 32];
204
+
205
+ // Derived inputs computed ONCE (matching the FFI side's setup).
206
+ let ws_frame = castrum::payload::ws_frames::encode_frame_into(
207
+ 1,
208
+ &ws_payload,
209
+ false,
210
+ true,
211
+ &mut ws_frame_out,
212
+ )
213
+ .unwrap();
214
+ let aead_ct =
215
+ castrum::crypto::aead::encrypt(&AES_256_GCM, &aead_key, &aead_nonce, &aead_pt).unwrap();
216
+ let argon_phc = castrum::crypto::argon2::hash_password(&password, &password_salt, 8, 1, 1, 16)
217
+ .unwrap()
218
+ .into_bytes();
219
+ let bcrypt_phc = bcrypt::hash(&password, castrum::crypto::bcrypt::clamp_cost(4))
220
+ .unwrap()
221
+ .into_bytes();
222
+ let signed_cookie_len = castrum::crypto::cookie_sign::sign_cookie_into(
223
+ &cookie_value,
224
+ &cookie_key,
225
+ &mut cookie_sig_out,
226
+ )
227
+ .unwrap();
228
+ let csrf_token_bytes: Vec<u8> = {
229
+ let mut rnd = [0u8; 32];
230
+ getrandom::fill(&mut rnd).unwrap();
231
+ let mut rnd_hex = [0u8; 64];
232
+ castrum::util::bytes::hex_encode(&rnd, &mut rnd_hex);
233
+ let mut sig_hex = [0u8; 64];
234
+ castrum::util::bytes::hex_encode(hmac::sign(&csrf_key, &rnd_hex).as_ref(), &mut sig_hex);
235
+ let mut t = Vec::with_capacity(129);
236
+ t.extend_from_slice(&rnd_hex);
237
+ t.push(b'.');
238
+ t.extend_from_slice(&sig_hex);
239
+ t
240
+ };
241
+
242
+ // ── Hash / checksum ──
243
+ bench("crc32", 1_000_000, || crc32_bytes(&crc) as u64);
244
+ bench("fnv1a64", 1_000_000, || fnv1a64_bytes(&crc) as u64);
245
+ bench("xxh3", 1_000_000, || fast_hash_bytes(&crc) as u64);
246
+
247
+ // ── Encode / decode ──
248
+ bench("hex_encode", 500_000, || {
249
+ hex_encode_into_slice(&encode, &mut hex_out).unwrap() as u64
250
+ });
251
+ bench("hex_decode", 500_000, || {
252
+ hex_decode_into_slice(&hex_in, &mut hex_dec_out).unwrap() as u64
253
+ });
254
+ bench("base64_encode", 500_000, || {
255
+ base64_encode_into_slice(&encode, &mut b64_out, false, true).unwrap() as u64
256
+ });
257
+ bench("base64_decode", 500_000, || {
258
+ base64_decode_into_slice(&base64_input, &mut b64_dec_out, false, true).unwrap() as u64
259
+ });
260
+ bench("url_encode", 500_000, || {
261
+ url_encode_into_slice(&url_enc, &mut url_out).unwrap() as u64
262
+ });
263
+ bench("url_decode", 500_000, || {
264
+ url_decode_into_slice(&url_dec, &mut urld_out).unwrap() as u64
265
+ });
266
+
267
+ // ── Validators ──
268
+ bench("validate_email", 1_000_000, || {
269
+ validate_email_bytes(&email) as u64
270
+ });
271
+ bench("validate_uuid", 1_000_000, || {
272
+ validate_uuid_bytes(&uuid) as u64
273
+ });
274
+ bench("validate_ipv4", 1_000_000, || {
275
+ validate_ipv4_bytes(&ipv4) as u64
276
+ });
277
+ bench("validate_ipv6", 1_000_000, || {
278
+ validate_ipv6_bytes(&ipv6) as u64
279
+ });
280
+
281
+ // ── JSON (zero-DOM) ──
282
+ bench("json_valid", 500_000, || json_valid_bytes(&json) as u64);
283
+ bench("json_sum_ids", 500_000, || {
284
+ json_sum_ids_bytes(&json).unwrap() as u64
285
+ });
286
+ // ── JSON DOM parse ALONE (no napi marshal) — the reference for the
287
+ // structural json_parse split: proves the sonic-rs parse is cheap and the
288
+ // ~5x CPU-bench loss is the napi DOM→JS marshal, not the parser. ──
289
+ bench("json_parse_dom", 200_000, || {
290
+ let v: serde_json::Value = sonic_rs::from_slice(&json).unwrap();
291
+ v.as_array().map_or(0, |a| a.len()) as u64
292
+ });
293
+
294
+ // ── ETag / HTTP-date / WebSocket accept ──
295
+ bench("etag", 500_000, || {
296
+ let crc = crc32_bytes(&etag_data);
297
+ etag_from_crc32_into(crc, false, &mut etag_out).unwrap() as u64
298
+ });
299
+ bench("http_date", 500_000, || {
300
+ http_date_into_slice(DEFAULT_HTTP_DATE_SECS, &mut date_out).unwrap() as u64
301
+ });
302
+ bench("ws_accept_key", 500_000, || {
303
+ ws_accept_key_into(&ws_key, &mut ws_out).unwrap() as u64
304
+ });
305
+
306
+ // ── SSE (write-into-buffer core) ──
307
+ let mut sse_out = [0u8; 512];
308
+ bench("sse_encode", 200_000, || {
309
+ encode_event_into_slice(
310
+ Some("update"),
311
+ &ws_payload,
312
+ Some("42"),
313
+ Some(3000),
314
+ &mut sse_out,
315
+ )
316
+ .unwrap() as u64
317
+ });
318
+
319
+ // ── Media type parse (allocating core — the reference for the napi-object
320
+ // vs JS-baseline loss; shows the parse itself is not the bottleneck). ──
321
+ bench("media_type_parse", 200_000, || {
322
+ let parsed = castrum::http::media_type::parse_media_type_core(&content_type).unwrap();
323
+ parsed.ty.len() as u64 + parsed.subtype.len() as u64
324
+ });
325
+
326
+ // ── HMAC (compiled key) ──
327
+ bench("hmac_sha256", 200_000, || {
328
+ let tag = hmac::sign(&hmac_key_compiled, &hmac_data);
329
+ hex_encode_into_slice(tag.as_ref(), &mut hmac_tag_out).unwrap() as u64
330
+ });
331
+ bench("hmac_sha256_verify", 200_000, || {
332
+ hex_decode_into_slice(&hmac_sig, &mut sig_bytes).unwrap();
333
+ u64::from(hmac::verify(&hmac_key_compiled, &hmac_data, &sig_bytes).is_ok())
334
+ });
335
+
336
+ // ── Cookies / CSRF ──
337
+ bench("sign_cookie", 300_000, || {
338
+ castrum::crypto::cookie_sign::sign_cookie_into(
339
+ &cookie_value,
340
+ &cookie_key,
341
+ &mut cookie_sig_out,
342
+ )
343
+ .unwrap_or(0) as u64
344
+ });
345
+ bench("verify_cookie", 300_000, || {
346
+ castrum::crypto::cookie_sign::verify_cookie_into(
347
+ &cookie_sig_out[..signed_cookie_len],
348
+ &cookie_key,
349
+ &mut cookie_val_out,
350
+ )
351
+ .unwrap_or(0) as u64
352
+ });
353
+ bench("csrf_token", 300_000, || {
354
+ let mut rnd = [0u8; 32];
355
+ getrandom::fill(&mut rnd).unwrap();
356
+ let mut rnd_hex = [0u8; 64];
357
+ castrum::util::bytes::hex_encode(&rnd, &mut rnd_hex);
358
+ let mut sig_hex = [0u8; 64];
359
+ castrum::util::bytes::hex_encode(hmac::sign(&csrf_key, &rnd_hex).as_ref(), &mut sig_hex);
360
+ csrf_out[..64].copy_from_slice(&rnd_hex);
361
+ csrf_out[64] = b'.';
362
+ csrf_out[65..129].copy_from_slice(&sig_hex);
363
+ 129
364
+ });
365
+ bench("csrf_verify", 300_000, || {
366
+ u64::from(castrum::crypto::csrf::csrf_verify_with_key(
367
+ &csrf_token_bytes,
368
+ &csrf_key,
369
+ ))
370
+ });
371
+
372
+ // ── Password KDFs (work-bound; low iterations) ──
373
+ bench("password_hash", 50, || {
374
+ castrum::crypto::argon2::hash_password(&password, &password_salt, 8, 1, 1, 16)
375
+ .unwrap()
376
+ .len() as u64
377
+ });
378
+ bench("password_verify", 50, || {
379
+ u64::from(castrum::crypto::argon2::verify_password(
380
+ &password, &argon_phc,
381
+ ))
382
+ });
383
+ bench("password_hash_bcrypt", 3, || {
384
+ bcrypt::hash(&password, castrum::crypto::bcrypt::clamp_cost(4))
385
+ .unwrap()
386
+ .len() as u64
387
+ });
388
+ bench("password_verify_bcrypt", 3, || {
389
+ u64::from(
390
+ bcrypt::verify(&password, std::str::from_utf8(&bcrypt_phc).unwrap()).unwrap_or(false),
391
+ )
392
+ });
393
+
394
+ // ── PBKDF2 ──
395
+ bench("pbkdf2_sha256", 200, || {
396
+ pbkdf2::pbkdf2_hmac::<sha2::Sha256>(&pbkdf2_pw, &pbkdf2_salt, 1, &mut pbkdf2_out);
397
+ pbkdf2_out.len() as u64
398
+ });
399
+
400
+ // ── AEAD ──
401
+ bench("aead_encrypt", 200_000, || {
402
+ castrum::crypto::aead::encrypt(&AES_256_GCM, &aead_key, &aead_nonce, &aead_pt)
403
+ .unwrap()
404
+ .len() as u64
405
+ });
406
+ bench("aead_decrypt", 200_000, || {
407
+ castrum::crypto::aead::decrypt(&AES_256_GCM, &aead_key, &aead_nonce, &aead_ct)
408
+ .unwrap()
409
+ .len() as u64
410
+ });
411
+
412
+ // ── WebSocket frames ──
413
+ bench("ws_frame_encode", 300_000, || {
414
+ castrum::payload::ws_frames::encode_frame_into(
415
+ 1,
416
+ &ws_payload,
417
+ false,
418
+ true,
419
+ &mut ws_frame_out,
420
+ )
421
+ .unwrap() as u64
422
+ });
423
+ bench("ws_frame_decode", 300_000, || {
424
+ let frame = castrum::payload::ws_frames::decode_frame(&ws_frame_out[..ws_frame]).unwrap();
425
+ frame.payload.len() as u64
426
+ });
427
+
428
+ // ── JSON patch ──
429
+ bench("json_patch", 200_000, || {
430
+ castrum::json::patch::apply_json_patch_bytes(&json_doc, &json_patch_data)
431
+ .unwrap()
432
+ .len() as u64
433
+ });
434
+
435
+ // ── Random token (getrandom + hex) ──
436
+ bench("random_token", 200_000, || {
437
+ let mut rnd = vec![0u8; 16];
438
+ getrandom::fill(&mut rnd).unwrap();
439
+ castrum::util::bytes::hex_encode(&rnd, &mut random_out);
440
+ 32
441
+ });
442
+
443
+ // ── Packed parsers ──
444
+ bench("http_parse_packed", 200_000, || {
445
+ http_parse_request_packed_into_slice(&http, &mut http_out).unwrap() as u64
446
+ });
447
+ bench("query_parse_packed", 200_000, || {
448
+ query_parse_packed_into_slice(&query, &mut query_out).unwrap() as u64
449
+ });
450
+ bench("cookie_parse_packed", 200_000, || {
451
+ cookie_parse_packed_into_slice(&cookie, &mut cookie_out).unwrap() as u64
452
+ });
453
+ bench("form_parse_packed", 200_000, || {
454
+ query_parse_packed_into_slice(&query, &mut query_out).unwrap() as u64
455
+ });
456
+ bench("multipart_parse_packed", 100_000, || {
457
+ let parts = castrum::http::multipart::parse_multipart_limited(
458
+ &multipart_body,
459
+ &multipart_boundary,
460
+ &Default::default(),
461
+ );
462
+ let mut buf = Vec::new();
463
+ castrum::http::multipart::parts_to_packed(&parts, &mut buf);
464
+ buf.len() as u64
465
+ });
466
+
467
+ // ── JWT sign (bytes) ──
468
+ bench("jwt_sign_bytes", 200_000, || {
469
+ let mut value: serde_json::Value = serde_json::from_slice(&jwt_claims).unwrap();
470
+ let payload_b64 =
471
+ castrum::crypto::jwt::inject_and_payload_b64(&mut value, None, 0).unwrap();
472
+ castrum::crypto::jwt::build_token(
473
+ castrum::crypto::jwt::jwt_header_b64(),
474
+ &payload_b64,
475
+ &jwt_secret,
476
+ )
477
+ .len() as u64
478
+ });
479
+
480
+ // ── Compression (write-into-buffer cores) ──
481
+ bench("gzip_compress", 20_000, || {
482
+ gzip_compress_into(&compress, 6, &mut gz_out).unwrap() as u64
483
+ });
484
+ bench("brotli_compress", 20_000, || {
485
+ brotli_compress_into(&compress, 5, &mut br_out).unwrap() as u64
486
+ });
487
+ if !gzip_compressed.is_empty() && !brotli_compressed.is_empty() {
488
+ bench("gzip_decompress", 20_000, || {
489
+ gzip_decompress_into(&gzip_compressed, 1024 * 1024, &mut gz_d_out).unwrap() as u64
490
+ });
491
+ bench("brotli_decompress", 20_000, || {
492
+ brotli_decompress_into(&brotli_compressed, 1024 * 1024, &mut br_d_out).unwrap() as u64
493
+ });
494
+ }
495
+ }
@@ -0,0 +1,119 @@
1
+ // rust/task/completion.rs — the completion ring + batched doorbell.
2
+ //
3
+ // THE performance-critical piece. A pooled native thread never returns to JS
4
+ // directly; it pushes a `Completion` here and (at most once per batch) calls a
5
+ // single thread-safe trampoline so the JS thread can drain everything in ONE
6
+ // `castrum_task_drain` call. That amortizes the ~666 ns/crossing threadsafe
7
+ // callback over a whole batch instead of paying it per task.
8
+ //
9
+ // PACKED DRAIN LAYOUT (`out`):
10
+ // [u32 count LE]
11
+ // repeat count: [u64 id LE][u32 status][u32 len][len bytes]
12
+ //
13
+ // Needed-size convention (repo-wide): when `out` is too small the EXACT
14
+ // required byte count is returned and nothing is consumed, so JS allocates
15
+ // once and retries once. `0` means "nothing pending".
16
+ //
17
+ // NOTE (2026-09-11): a pointer-based zero-copy variant was prototyped and
18
+ // REVERTED — Bun 1.4.2's `toArrayBuffer` deallocator hook segfaults (see
19
+ // docs/RND-CONCURRENCY.md §7a). Zero-copy will instead pre-allocate the
20
+ // destination in JS and have the pool thread write into it directly.
21
+
22
+ use parking_lot::Mutex;
23
+ use std::collections::VecDeque;
24
+ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
25
+ use std::sync::OnceLock;
26
+
27
+ /// One finished task. `status`: 0 = ok, 1 = error, 2 = cancelled. `body` is
28
+ /// the result bytes, or the UTF-8 error message when `status != 0`.
29
+ pub struct Completion {
30
+ pub id: u64,
31
+ pub status: u32,
32
+ pub body: Vec<u8>,
33
+ }
34
+
35
+ /// Completions waiting to be drained. Lazily initialised WITH capacity so a
36
+ /// burst (hundreds of tasks finishing together) does not pay repeated VecDeque
37
+ /// growth reallocations on the worker threads.
38
+ static RING: OnceLock<Mutex<VecDeque<Completion>>> = OnceLock::new();
39
+
40
+ /// One burst's worth of entries before the first realloc.
41
+ const RING_CAPACITY: usize = 256;
42
+
43
+ fn ring() -> &'static Mutex<VecDeque<Completion>> {
44
+ RING.get_or_init(|| Mutex::new(VecDeque::with_capacity(RING_CAPACITY)))
45
+ }
46
+ /// `true` while a doorbell call is outstanding, so N completions in a burst
47
+ /// wake JS once, not N times.
48
+ static ARMED: AtomicBool = AtomicBool::new(false);
49
+ /// Address of the thread-safe trampoline (`JSCallback({threadsafe:true}).ptr`).
50
+ static DOORBELL: AtomicUsize = AtomicUsize::new(0);
51
+
52
+ /// Register the doorbell trampoline. `0` disables it (poll-only mode).
53
+ pub fn set_doorbell(cb: usize) {
54
+ DOORBELL.store(cb, Ordering::Release);
55
+ }
56
+
57
+ /// Push a completion and ring the doorbell at most once per armed window.
58
+ ///
59
+ /// The ring lock is released BEFORE the trampoline is called: the JS callback
60
+ /// re-enters `drain`, which locks the ring again — holding it here would
61
+ /// deadlock the process.
62
+ pub fn push(c: Completion) {
63
+ let should_ring = {
64
+ let mut q = ring().lock();
65
+ q.push_back(c);
66
+ !ARMED.swap(true, Ordering::AcqRel)
67
+ };
68
+ if should_ring {
69
+ let p = DOORBELL.load(Ordering::Acquire);
70
+ if p != 0 {
71
+ // SAFETY: `p` is a live `JSCallback` thread-safe trampoline whose
72
+ // JS owner is held for the process lifetime by the task runtime.
73
+ let f: extern "C" fn() = unsafe { std::mem::transmute(p) };
74
+ f();
75
+ }
76
+ }
77
+ }
78
+
79
+ /// Drain every pending completion into `out`. Returns bytes written, `0` when
80
+ /// nothing is pending, or the exact required size when `out` is too small.
81
+ pub fn drain(out: &mut [u8]) -> usize {
82
+ // Disarm FIRST so a completion pushed while we drain re-arms and rings the
83
+ // bell instead of being stranded (see the module note).
84
+ ARMED.store(false, Ordering::Release);
85
+ let mut q = ring().lock();
86
+ if q.is_empty() {
87
+ return 0;
88
+ }
89
+ let mut needed = 4usize;
90
+ for c in q.iter() {
91
+ needed += 16 + c.body.len();
92
+ }
93
+ if out.len() < needed {
94
+ return needed;
95
+ }
96
+ out[0..4].copy_from_slice(&(q.len() as u32).to_le_bytes());
97
+ let mut off = 4usize;
98
+ for c in q.iter() {
99
+ out[off..off + 8].copy_from_slice(&c.id.to_le_bytes());
100
+ out[off + 8..off + 12].copy_from_slice(&c.status.to_le_bytes());
101
+ out[off + 12..off + 16].copy_from_slice(&(c.body.len() as u32).to_le_bytes());
102
+ out[off + 16..off + 16 + c.body.len()].copy_from_slice(&c.body);
103
+ off += 16 + c.body.len();
104
+ }
105
+ q.clear();
106
+ needed
107
+ }
108
+
109
+ /// Completions waiting to be drained.
110
+ pub fn pending() -> u32 {
111
+ ring().lock().len() as u32
112
+ }
113
+
114
+ /// Drop every pending completion (shutdown / test isolation). Bodies already
115
+ /// handed to JS by `drain_ptrs` are owned by JS and are NOT freed here.
116
+ pub fn clear() {
117
+ ring().lock().clear();
118
+ ARMED.store(false, Ordering::Release);
119
+ }
@@ -0,0 +1,65 @@
1
+ // rust/task — off-thread task runtime ("castrum Tasks").
2
+ //
3
+ // A Bun-first answer to synchronous FFI: any CPU-bound native op that would
4
+ // otherwise freeze the JS thread is submitted as a task, runs on a dedicated
5
+ // pool thread (NOT the rayon batch pool), and resolves a JS promise through a
6
+ // BATCHED completion doorbell. See docs/RND-CONCURRENCY.md for the design and
7
+ // the measured Bun concurrency costs.
8
+ //
9
+ // runtime.rs pool lifecycle (once-per-process) + admission
10
+ // completion.rs result ring + batched thread-safe doorbell (the fast path)
11
+ // ops.rs op dispatch table + cancellation + panic containment
12
+ // tests.rs pool/ring/cancel/panic/doorbell unit tests
13
+ //
14
+ // Safety contract (identical to the rest of the C-ABI surface): every task body
15
+ // runs under `catch_unwind`; no `Mutex` is held across the doorbell call; the
16
+ // pool owns every input buffer after `submit` returns.
17
+
18
+ pub mod completion;
19
+ pub mod ops;
20
+ pub mod runtime;
21
+
22
+ #[cfg(test)]
23
+ mod tests;
24
+
25
+ pub use completion::{drain, pending, set_doorbell};
26
+ pub use ops::{mark_cancelled, STATUS_CANCELLED, STATUS_ERROR, STATUS_OK};
27
+ pub use runtime::{init, shutdown, threads};
28
+
29
+ /// Submit one task op with packed `args`, completing under `id`.
30
+ ///
31
+ /// The pool auto-starts (default worker count) on first use, so callers do not
32
+ /// have to sequence `init` before `submit`. `args` is moved onto the worker.
33
+ pub fn submit_op(op: u32, args: Vec<u8>, id: u64) -> bool {
34
+ runtime::submit(move || ops::execute_guarded(op, &args, id))
35
+ }
36
+
37
+ /// Submit one zero-copy task op: the pool thread writes its result DIRECTLY
38
+ /// into the caller's `out` buffer, and the completion body is the 8-byte LE
39
+ /// written length instead of the data. This removes the last full-size copy and
40
+ /// its allocation from the JS thread (see docs/RND-CONCURRENCY.md §7a).
41
+ ///
42
+ /// # Safety
43
+ /// `out` must point to a buffer of at least `out_cap` bytes that stays alive
44
+ /// and is not touched by JS until the completion for `id` is drained.
45
+ pub fn submit_op_into(op: u32, args: Vec<u8>, id: u64, out: usize, out_cap: usize) -> bool {
46
+ runtime::submit(move || unsafe {
47
+ ops::execute_into_guarded(op, &args, id, out as *mut u8, out_cap)
48
+ })
49
+ }
50
+
51
+ /// Submit one task whose PAYLOAD is read IN PLACE from the caller's buffer
52
+ /// (zero-copy INPUT): only the `hdr` header bytes are copied, so a large
53
+ /// argument never crosses the boundary. Without this, offloading a large
54
+ /// op pays a full-size copy on the JS thread — measured to make
55
+ /// `gzipCompress` of 24 MiB SLOWER than the synchronous built-in it replaces
56
+ /// (18.6 ms / 11.7 ms stall vs 5.6 ms), which is the opposite of the point.
57
+ ///
58
+ /// # Safety
59
+ /// `data`/`data_len` must describe a readable region that stays alive and
60
+ /// unmodified until the completion for `id` is drained.
61
+ pub fn submit_slice_op(op: u32, hdr: Vec<u8>, data: usize, data_len: usize, id: u64) -> bool {
62
+ runtime::submit(move || unsafe {
63
+ ops::execute_slice_guarded(op, &hdr, data as *const u8, data_len, id)
64
+ })
65
+ }