castrum 0.9.10 → 0.9.11

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.
@@ -7,19 +7,20 @@
7
7
  use std::ptr;
8
8
  use std::slice;
9
9
 
10
- use super::util::panic_guard;
11
-
10
+ use super::util::{c_abi, panic_guard};
12
11
  /// CRC32 over `data[0..len]`. Returns the CRC-32 checksum.
13
12
  ///
14
13
  /// # Safety
15
14
  /// `data` must be valid for reads of `len` bytes.
16
15
  #[no_mangle]
17
16
  pub unsafe extern "C" fn castrum_crc32(data: *const u8, len: usize) -> u32 {
18
- if data.is_null() && len != 0 {
19
- return 0;
20
- }
21
- let bytes = slice::from_raw_parts(data, len);
22
- crate::crypto::hashing::crc32_bytes(bytes)
17
+ c_abi!(0, {
18
+ if data.is_null() && len != 0 {
19
+ return 0;
20
+ }
21
+ let bytes = slice::from_raw_parts(data, len);
22
+ crate::crypto::hashing::crc32_bytes(bytes)
23
+ })
23
24
  }
24
25
 
25
26
  /// FNV-1a 64 over `data[0..len]`.
@@ -28,11 +29,13 @@ pub unsafe extern "C" fn castrum_crc32(data: *const u8, len: usize) -> u32 {
28
29
  /// `data` must be valid for reads of `len` bytes.
29
30
  #[no_mangle]
30
31
  pub unsafe extern "C" fn castrum_fnv1a64(data: *const u8, len: usize) -> u64 {
31
- if data.is_null() && len != 0 {
32
- return 0;
33
- }
34
- let bytes = slice::from_raw_parts(data, len);
35
- crate::crypto::hashing::fnv1a64_bytes(bytes)
32
+ c_abi!(0, {
33
+ if data.is_null() && len != 0 {
34
+ return 0;
35
+ }
36
+ let bytes = slice::from_raw_parts(data, len);
37
+ crate::crypto::hashing::fnv1a64_bytes(bytes)
38
+ })
36
39
  }
37
40
 
38
41
  /// XXH3-64 over `data[0..len]`.
@@ -41,11 +44,13 @@ pub unsafe extern "C" fn castrum_fnv1a64(data: *const u8, len: usize) -> u64 {
41
44
  /// `data` must be valid for reads of `len` bytes.
42
45
  #[no_mangle]
43
46
  pub unsafe extern "C" fn castrum_xxh3(data: *const u8, len: usize) -> u64 {
44
- if data.is_null() && len != 0 {
45
- return 0;
46
- }
47
- let bytes = slice::from_raw_parts(data, len);
48
- crate::crypto::hashing::fast_hash_bytes(bytes)
47
+ c_abi!(0, {
48
+ if data.is_null() && len != 0 {
49
+ return 0;
50
+ }
51
+ let bytes = slice::from_raw_parts(data, len);
52
+ crate::crypto::hashing::fast_hash_bytes(bytes)
53
+ })
49
54
  }
50
55
 
51
56
  /// JSON-validity check over `data[0..len]`. Returns 1 if the bytes are
@@ -55,11 +60,13 @@ pub unsafe extern "C" fn castrum_xxh3(data: *const u8, len: usize) -> u64 {
55
60
  /// `data` must be valid for reads of `len` bytes.
56
61
  #[no_mangle]
57
62
  pub unsafe extern "C" fn castrum_json_valid(data: *const u8, len: usize) -> u8 {
58
- if data.is_null() {
59
- return 0;
60
- }
61
- let bytes = slice::from_raw_parts(data, len);
62
- u8::from(crate::json::json_ops::json_valid_bytes(bytes))
63
+ c_abi!(0, {
64
+ if data.is_null() {
65
+ return 0;
66
+ }
67
+ let bytes = slice::from_raw_parts(data, len);
68
+ u8::from(crate::json::json_ops::json_valid_bytes(bytes))
69
+ })
63
70
  }
64
71
 
65
72
  /// UTF-8 validity check over `data[0..len]`. Returns 1 if the bytes are valid
@@ -70,11 +77,13 @@ pub unsafe extern "C" fn castrum_json_valid(data: *const u8, len: usize) -> u8 {
70
77
  /// `data` must be valid for reads of `len` bytes.
71
78
  #[no_mangle]
72
79
  pub unsafe extern "C" fn castrum_utf8_valid(data: *const u8, len: usize) -> u8 {
73
- if data.is_null() {
74
- return 0;
75
- }
76
- let bytes = slice::from_raw_parts(data, len);
77
- u8::from(std::str::from_utf8(bytes).is_ok())
80
+ c_abi!(0, {
81
+ if data.is_null() {
82
+ return 0;
83
+ }
84
+ let bytes = slice::from_raw_parts(data, len);
85
+ u8::from(std::str::from_utf8(bytes).is_ok())
86
+ })
78
87
  }
79
88
 
80
89
  /// Lowercase-hex encode `data[0..len]` into `out[0..out_cap]`. Returns bytes
@@ -90,12 +99,14 @@ pub unsafe extern "C" fn castrum_hex_encode(
90
99
  out: *mut u8,
91
100
  out_cap: usize,
92
101
  ) -> usize {
93
- if data.is_null() || out.is_null() || len.checked_mul(2).is_none_or(|n| n > out_cap) {
94
- return 0;
95
- }
96
- let input = slice::from_raw_parts(data, len);
97
- let output = slice::from_raw_parts_mut(out, len * 2);
98
- crate::crypto::base64::hex_encode_into_slice(input, output).unwrap_or(0)
102
+ c_abi!(0, {
103
+ if data.is_null() || out.is_null() || len.checked_mul(2).is_none_or(|n| n > out_cap) {
104
+ return 0;
105
+ }
106
+ let input = slice::from_raw_parts(data, len);
107
+ let output = slice::from_raw_parts_mut(out, len * 2);
108
+ crate::crypto::base64::hex_encode_into_slice(input, output).unwrap_or(0)
109
+ })
99
110
  }
100
111
 
101
112
  /// RFC 3986 percent-encode `data[0..len]` into `out[0..out_cap]`. Returns
@@ -112,12 +123,14 @@ pub unsafe extern "C" fn castrum_url_encode(
112
123
  out: *mut u8,
113
124
  out_cap: usize,
114
125
  ) -> usize {
115
- if data.is_null() || out.is_null() {
116
- return 0;
117
- }
118
- let input = slice::from_raw_parts(data, len);
119
- let output = slice::from_raw_parts_mut(out, out_cap);
120
- crate::http::url_codec::url_encode_into_slice(input, output).unwrap_or_default()
126
+ c_abi!(0, {
127
+ if data.is_null() || out.is_null() {
128
+ return 0;
129
+ }
130
+ let input = slice::from_raw_parts(data, len);
131
+ let output = slice::from_raw_parts_mut(out, out_cap);
132
+ crate::http::url_codec::url_encode_into_slice(input, output).unwrap_or_default()
133
+ })
121
134
  }
122
135
 
123
136
  /// Hex-decode `data[0..len]` into `out[0..out_cap]`. Returns bytes written
@@ -133,12 +146,14 @@ pub unsafe extern "C" fn castrum_hex_decode(
133
146
  out: *mut u8,
134
147
  out_cap: usize,
135
148
  ) -> usize {
136
- if data.is_null() || out.is_null() {
137
- return 0;
138
- }
139
- let input = slice::from_raw_parts(data, len);
140
- let output = slice::from_raw_parts_mut(out, out_cap);
141
- crate::crypto::base64::hex_decode_into_slice(input, output).unwrap_or_default()
149
+ c_abi!(0, {
150
+ if data.is_null() || out.is_null() {
151
+ return 0;
152
+ }
153
+ let input = slice::from_raw_parts(data, len);
154
+ let output = slice::from_raw_parts_mut(out, out_cap);
155
+ crate::crypto::base64::hex_decode_into_slice(input, output).unwrap_or_default()
156
+ })
142
157
  }
143
158
 
144
159
  /// Percent-decode `data[0..len]` into `out[0..out_cap]`. Returns bytes
@@ -154,12 +169,14 @@ pub unsafe extern "C" fn castrum_url_decode(
154
169
  out: *mut u8,
155
170
  out_cap: usize,
156
171
  ) -> usize {
157
- if data.is_null() || out.is_null() {
158
- return 0;
159
- }
160
- let input = slice::from_raw_parts(data, len);
161
- let output = slice::from_raw_parts_mut(out, out_cap);
162
- crate::http::url_codec::url_decode_into_slice(input, output).unwrap_or_default()
172
+ c_abi!(0, {
173
+ if data.is_null() || out.is_null() {
174
+ return 0;
175
+ }
176
+ let input = slice::from_raw_parts(data, len);
177
+ let output = slice::from_raw_parts_mut(out, out_cap);
178
+ crate::http::url_codec::url_decode_into_slice(input, output).unwrap_or_default()
179
+ })
163
180
  }
164
181
 
165
182
  /// Sum of `id` fields across a JSON array (sonic-rs zero-DOM) → packed
package/rust/ffi/http.rs CHANGED
@@ -6,8 +6,7 @@
6
6
 
7
7
  use std::slice;
8
8
 
9
- use super::util::{cstring_return, panic_guard};
10
-
9
+ use super::util::{c_abi, cstring_return, panic_guard};
11
10
  /// crc32-based ETag (10 strong / 12 weak) into `out`; `weak` is a u8 flag.
12
11
  ///
13
12
  /// # Safety
@@ -22,13 +21,15 @@ pub unsafe extern "C" fn castrum_etag(
22
21
  len: usize,
23
22
  weak: u8,
24
23
  ) -> *const std::os::raw::c_char {
25
- if data.is_null() {
26
- return std::ptr::null();
27
- }
28
- let crc = crc32fast::hash(slice::from_raw_parts(data, len));
29
- let needed = if weak != 0 { 12 } else { 10 };
30
- cstring_return(needed, |out| {
31
- crate::http::etag::etag_from_crc32_into(crc, weak != 0, out).ok()
24
+ c_abi!(std::ptr::null(), {
25
+ if data.is_null() {
26
+ return std::ptr::null();
27
+ }
28
+ let crc = crc32fast::hash(slice::from_raw_parts(data, len));
29
+ let needed = if weak != 0 { 12 } else { 10 };
30
+ cstring_return(needed, |out| {
31
+ crate::http::etag::etag_from_crc32_into(crc, weak != 0, out).ok()
32
+ })
32
33
  })
33
34
  }
34
35
 
@@ -47,16 +48,22 @@ pub unsafe extern "C" fn castrum_etag_into(
47
48
  out: *mut u8,
48
49
  out_cap: usize,
49
50
  ) -> usize {
50
- if data.is_null() || out.is_null() {
51
- return 0;
52
- }
53
- let needed = if weak != 0 { 12 } else { 10 };
54
- if out_cap < needed {
55
- return needed;
56
- }
57
- let crc = crc32fast::hash(slice::from_raw_parts(data, len));
58
- crate::http::etag::etag_from_crc32_into(crc, weak != 0, slice::from_raw_parts_mut(out, needed))
51
+ c_abi!(0, {
52
+ if data.is_null() || out.is_null() {
53
+ return 0;
54
+ }
55
+ let needed = if weak != 0 { 12 } else { 10 };
56
+ if out_cap < needed {
57
+ return needed;
58
+ }
59
+ let crc = crc32fast::hash(slice::from_raw_parts(data, len));
60
+ crate::http::etag::etag_from_crc32_into(
61
+ crc,
62
+ weak != 0,
63
+ slice::from_raw_parts_mut(out, needed),
64
+ )
59
65
  .unwrap_or_default()
66
+ })
60
67
  }
61
68
 
62
69
  /// Evaluate `ConditionalRequest::is_not_modified` against the precompiled state
@@ -227,11 +234,13 @@ pub unsafe extern "C" fn castrum_accept_negotiator_negotiate_server(
227
234
  /// `out` must be valid for writes of up to `out_cap` bytes.
228
235
  #[no_mangle]
229
236
  pub unsafe extern "C" fn castrum_http_date_into(secs: f64, out: *mut u8, out_cap: usize) -> usize {
230
- if out.is_null() {
231
- return 0;
232
- }
233
- let output = slice::from_raw_parts_mut(out, out_cap);
234
- crate::http::http_date::http_date_into_slice(secs as i64, output).unwrap_or(0)
237
+ c_abi!(0, {
238
+ if out.is_null() {
239
+ return 0;
240
+ }
241
+ let output = slice::from_raw_parts_mut(out, out_cap);
242
+ crate::http::http_date::http_date_into_slice(secs as i64, output).unwrap_or(0)
243
+ })
235
244
  }
236
245
 
237
246
  /// Parse an IMF-fixdate back to unix seconds → packed `[u8 ok][i64 secs LE]`
@@ -721,7 +730,7 @@ pub unsafe extern "C" fn castrum_form_parse_packed(
721
730
  out: *mut u8,
722
731
  out_cap: usize,
723
732
  ) -> usize {
724
- castrum_query_parse_packed(data, len, out, out_cap)
733
+ c_abi!(0, { castrum_query_parse_packed(data, len, out, out_cap) })
725
734
  }
726
735
 
727
736
  /// Parse a `multipart/form-data` body into the packed parts layout (the same
@@ -8,8 +8,7 @@
8
8
 
9
9
  use std::slice;
10
10
 
11
- use super::util::panic_guard;
12
-
11
+ use super::util::{c_abi, panic_guard};
13
12
  /// Run the ingress pipeline on a packed request frame. `inner` is the opaque
14
13
  /// pointer (as an integer) from `Ingress.ingressInnerPtr()` — valid ONLY while
15
14
  /// the `Ingress` instance is alive (see the method's safety note). This is the
@@ -290,14 +289,16 @@ impl IngressLayout {
290
289
  /// `out` must be valid for writes up to `out_cap`.
291
290
  #[no_mangle]
292
291
  pub unsafe extern "C" fn castrum_ingress_layout(out: *mut u8, out_cap: usize) -> usize {
293
- if out.is_null() {
294
- return 0;
295
- }
296
- const LAYOUT: IngressLayout = IngressLayout::from_output();
297
- let n = core::mem::size_of::<IngressLayout>();
298
- if out_cap < n {
299
- return 0;
300
- }
301
- core::ptr::copy_nonoverlapping(&LAYOUT as *const IngressLayout as *const u8, out, n);
302
- n
292
+ c_abi!(0, {
293
+ if out.is_null() {
294
+ return 0;
295
+ }
296
+ const LAYOUT: IngressLayout = IngressLayout::from_output();
297
+ let n = core::mem::size_of::<IngressLayout>();
298
+ if out_cap < n {
299
+ return 0;
300
+ }
301
+ core::ptr::copy_nonoverlapping(&LAYOUT as *const IngressLayout as *const u8, out, n);
302
+ n
303
+ })
303
304
  }
@@ -10,8 +10,7 @@
10
10
 
11
11
  use std::slice;
12
12
 
13
- use super::util::panic_guard;
14
-
13
+ use super::util::{c_abi, panic_guard};
15
14
  /// `u32::MAX` — the declare-failure sentinel (fits f64 exactly; real series
16
15
  /// ids are dense from 0 and capped well below this).
17
16
  pub(crate) const METRICS_DECLARE_ERR: u32 = u32::MAX;
@@ -294,11 +293,13 @@ pub unsafe extern "C" fn castrum_metrics_render(
294
293
  /// `handle` must be either 0 or a live handle not yet destroyed.
295
294
  #[no_mangle]
296
295
  pub unsafe extern "C" fn castrum_metrics_destroy(handle: usize) {
297
- if handle != 0 {
298
- drop(Box::from_raw(
299
- handle as *mut crate::metrics::MetricsRegistry,
300
- ));
301
- }
296
+ c_abi!((), {
297
+ if handle != 0 {
298
+ drop(Box::from_raw(
299
+ handle as *mut crate::metrics::MetricsRegistry,
300
+ ));
301
+ }
302
+ })
302
303
  }
303
304
 
304
305
  /// Split a `\x1f`-separated key list into owned strings (empty input → []).
@@ -6,8 +6,7 @@
6
6
 
7
7
  use std::slice;
8
8
 
9
- use super::util::{cstring_return, panic_guard};
10
-
9
+ use super::util::{c_abi, cstring_return, panic_guard};
11
10
  /// RFC 6455 Sec-WebSocket-Accept (28 bytes) returned as a null-terminated C
12
11
  /// string into the per-thread reused buffer (`cstring` return).
13
12
  ///
@@ -52,19 +51,21 @@ pub unsafe extern "C" fn castrum_ws_accept_key_into(
52
51
  out: *mut u8,
53
52
  out_cap: usize,
54
53
  ) -> usize {
55
- if key.is_null() || out.is_null() {
56
- return 0;
57
- }
58
- if out_cap < 28 {
59
- return 28;
60
- }
61
- match crate::payload::websocket::ws_accept_key_into(
62
- slice::from_raw_parts(key, klen),
63
- slice::from_raw_parts_mut(out, 28),
64
- ) {
65
- Ok(28) => 28,
66
- _ => 0,
67
- }
54
+ c_abi!(0, {
55
+ if key.is_null() || out.is_null() {
56
+ return 0;
57
+ }
58
+ if out_cap < 28 {
59
+ return 28;
60
+ }
61
+ match crate::payload::websocket::ws_accept_key_into(
62
+ slice::from_raw_parts(key, klen),
63
+ slice::from_raw_parts_mut(out, 28),
64
+ ) {
65
+ Ok(28) => 28,
66
+ _ => 0,
67
+ }
68
+ })
68
69
  }
69
70
 
70
71
  /// WebSocket frame encode into `out` (opcode 1/2/8/9/10, `mask`/`fin` flags).
@@ -216,14 +217,16 @@ compress_to_out!(
216
217
  /// `data` must be valid for reads of `len` bytes.
217
218
  #[no_mangle]
218
219
  pub unsafe extern "C" fn castrum_gzip_isize(data: *const u8, len: usize) -> u32 {
219
- if data.is_null() || len < 18 {
220
- return 0;
221
- }
222
- let s = slice::from_raw_parts(data, len);
223
- if s[0] != 0x1f || s[1] != 0x8b {
224
- return 0;
225
- }
226
- u32::from_le_bytes([s[len - 4], s[len - 3], s[len - 2], s[len - 1]])
220
+ c_abi!(0, {
221
+ if data.is_null() || len < 18 {
222
+ return 0;
223
+ }
224
+ let s = slice::from_raw_parts(data, len);
225
+ if s[0] != 0x1f || s[1] != 0x8b {
226
+ return 0;
227
+ }
228
+ u32::from_le_bytes([s[len - 4], s[len - 3], s[len - 2], s[len - 1]])
229
+ })
227
230
  }
228
231
 
229
232
  /// Encode one SSE event into `out`. Optional fields use `flags` bits
package/rust/ffi/probe.rs CHANGED
@@ -15,15 +15,18 @@
15
15
 
16
16
  use std::slice;
17
17
 
18
+ use super::util::c_abi;
18
19
  /// Bare C-ABI trampoline floor: does nothing, returns nothing.
19
20
  #[no_mangle]
20
- pub extern "C" fn ffi_probe_noop() {}
21
+ pub extern "C" fn ffi_probe_noop() {
22
+ c_abi!((), {})
23
+ }
21
24
 
22
25
  /// Scalar pass-through: returns `v` unchanged. Measures scalar-arg + return
23
26
  /// conversion only (bind with `usize` vs `u64_fast` to isolate BigInt boxing).
24
27
  #[no_mangle]
25
28
  pub extern "C" fn ffi_probe_echo_usize(v: usize) -> usize {
26
- v
29
+ c_abi!(0, { v })
27
30
  }
28
31
 
29
32
  /// View pass-through: returns the byte length of the `(ptr, len)` pair.
@@ -34,11 +37,13 @@ pub extern "C" fn ffi_probe_echo_usize(v: usize) -> usize {
34
37
  /// form a slice whose length we return — never dereferenced).
35
38
  #[no_mangle]
36
39
  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
40
+ c_abi!(0, {
41
+ if data.is_null() && len != 0 {
42
+ return 0;
43
+ }
44
+ let _ = slice::from_raw_parts(data, len);
45
+ len
46
+ })
42
47
  }
43
48
 
44
49
  /// `cstring`-ARG pass-through: returns the byte length of the NUL-terminated
@@ -53,8 +58,10 @@ pub unsafe extern "C" fn ffi_probe_echo_view(data: *const u8, len: usize) -> usi
53
58
  /// `data` must be a valid NUL-terminated C string for reads up to its terminator.
54
59
  #[no_mangle]
55
60
  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()
61
+ c_abi!(0, {
62
+ if data.is_null() {
63
+ return 0;
64
+ }
65
+ std::ffi::CStr::from_ptr(data).to_bytes().len()
66
+ })
60
67
  }
package/rust/ffi/route.rs CHANGED
@@ -11,8 +11,7 @@
11
11
 
12
12
  use std::slice;
13
13
 
14
- use super::util::panic_guard;
15
-
14
+ use super::util::{c_abi, panic_guard};
16
15
  /// Compile a route descriptor into an opaque handle (`0` = failure / null
17
16
  /// input / panic). The handle is an owned `Box<NativeRoute>`; release it with
18
17
  /// {@link castrum_route_destroy}.
@@ -79,7 +78,9 @@ pub unsafe extern "C" fn castrum_route_run(
79
78
  /// most once.
80
79
  #[no_mangle]
81
80
  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
- }
81
+ c_abi!((), {
82
+ if handle != 0 {
83
+ drop(Box::from_raw(handle as *mut crate::ingress::NativeRoute));
84
+ }
85
+ })
85
86
  }
package/rust/ffi/task.rs CHANGED
@@ -7,9 +7,8 @@
7
7
 
8
8
  use std::slice;
9
9
 
10
- use super::util::panic_guard;
10
+ use super::util::{c_abi, panic_guard};
11
11
  use crate::task;
12
-
13
12
  /// Start the pool with `threads` workers (0 → runtime default).
14
13
  /// Returns `0` started, `1` already running, `2` internal error.
15
14
  #[no_mangle]
@@ -67,7 +66,7 @@ pub unsafe extern "C" fn castrum_task_drain(out: *mut u8, out_cap: usize) -> usi
67
66
  /// Completions waiting to be drained.
68
67
  #[no_mangle]
69
68
  pub extern "C" fn castrum_task_pending() -> u32 {
70
- task::pending()
69
+ c_abi!(0, { task::pending() })
71
70
  }
72
71
 
73
72
  /// Submit an op that writes its result DIRECTLY into `out` (zero-copy output):
@@ -115,7 +114,7 @@ pub unsafe extern "C" fn castrum_task_submit_out(
115
114
  /// Mark `task_id` cancelled. Returns `1` when newly marked, `0` otherwise.
116
115
  #[no_mangle]
117
116
  pub extern "C" fn castrum_task_cancel(task_id: usize) -> u32 {
118
- u32::from(task::mark_cancelled(task_id as u64))
117
+ c_abi!(0, { u32::from(task::mark_cancelled(task_id as u64)) })
119
118
  }
120
119
 
121
120
  /// Submit an op whose PAYLOAD is read IN PLACE from the caller's buffer
@@ -169,8 +168,10 @@ pub unsafe extern "C" fn castrum_task_submit_slice(
169
168
  /// thread (a `bun:ffi` `JSCallback({ threadsafe: true }).ptr`).
170
169
  #[no_mangle]
171
170
  pub extern "C" fn castrum_task_set_doorbell(cb: *const std::os::raw::c_void) -> u32 {
172
- task::set_doorbell(cb as usize);
173
- 1
171
+ c_abi!(0, {
172
+ task::set_doorbell(cb as usize);
173
+ 1
174
+ })
174
175
  }
175
176
 
176
177
  /// Stop the pool and drop pending completions. Returns `0`.
@@ -179,14 +180,16 @@ pub extern "C" fn castrum_task_set_doorbell(cb: *const std::os::raw::c_void) ->
179
180
  /// point must not call a JS callback the owner is about to close.
180
181
  #[no_mangle]
181
182
  pub extern "C" fn castrum_task_shutdown() -> u32 {
182
- task::completion::clear_doorbell();
183
- task::shutdown();
184
- task::completion::clear();
185
- 0
183
+ c_abi!(0, {
184
+ task::completion::clear_doorbell();
185
+ task::shutdown();
186
+ task::completion::clear();
187
+ 0
188
+ })
186
189
  }
187
190
 
188
191
  /// Configured pool worker count (0 before the pool starts).
189
192
  #[no_mangle]
190
193
  pub extern "C" fn castrum_task_threads() -> u32 {
191
- task::threads()
194
+ c_abi!(0, { task::threads() })
192
195
  }
package/rust/ffi/tests.rs CHANGED
@@ -2528,3 +2528,98 @@ fn metrics_record_batch_c_abi() {
2528
2528
  assert!(text.contains("batch_total{k=\"v\"} 4"));
2529
2529
  unsafe { castrum_metrics_destroy(h) };
2530
2530
  }
2531
+
2532
+ // ── Panic-containment contract ─────────────────────────────────
2533
+
2534
+ /// Every `#[no_mangle]` export must route through `c_abi!`/`panic_guard`.
2535
+ ///
2536
+ /// A panic unwinding through an `extern "C"` frame is undefined behaviour and
2537
+ /// aborts the host process (Bun), which is exactly the failure mode the
2538
+ /// `11-concurrent-burst` crash exposed. This source-level guard makes a new,
2539
+ /// unguarded export a test failure rather than a production SIGABRT.
2540
+ #[test]
2541
+ fn every_c_abi_export_is_panic_guarded() {
2542
+ const SOURCES: &[(&str, &str)] = &[
2543
+ ("crypto.rs", include_str!("crypto.rs")),
2544
+ ("hashing.rs", include_str!("hashing.rs")),
2545
+ ("http.rs", include_str!("http.rs")),
2546
+ ("ingress.rs", include_str!("ingress.rs")),
2547
+ ("json.rs", include_str!("json.rs")),
2548
+ ("jwt.rs", include_str!("jwt.rs")),
2549
+ ("metrics.rs", include_str!("metrics.rs")),
2550
+ ("payload.rs", include_str!("payload.rs")),
2551
+ ("probe.rs", include_str!("probe.rs")),
2552
+ ("rate_limit.rs", include_str!("rate_limit.rs")),
2553
+ ("route.rs", include_str!("route.rs")),
2554
+ ("task.rs", include_str!("task.rs")),
2555
+ ("validators.rs", include_str!("validators.rs")),
2556
+ ];
2557
+
2558
+ let mut missing = Vec::new();
2559
+ for (file, source) in SOURCES {
2560
+ let mut rest = *source;
2561
+ while let Some(idx) = rest.find("#[no_mangle]") {
2562
+ let item = &rest[idx..];
2563
+ let next = item[1..]
2564
+ .find("#[no_mangle]")
2565
+ .map(|i| i + 1)
2566
+ .unwrap_or(item.len());
2567
+ let guarded = item[..next].contains("c_abi!") || item[..next].contains("panic_guard");
2568
+ if !guarded {
2569
+ let name = item
2570
+ .split("fn ")
2571
+ .nth(1)
2572
+ .and_then(|s| s.split(['(', '<', ' ']).next())
2573
+ .unwrap_or("<unknown>");
2574
+ missing.push(format!("{file}:{name}"));
2575
+ }
2576
+ rest = &item[next..];
2577
+ }
2578
+ }
2579
+
2580
+ assert!(
2581
+ missing.is_empty(),
2582
+ "C-ABI exports without a panic guard: {missing:?}"
2583
+ );
2584
+ }
2585
+
2586
+ /// Malformed-input fuzz over the packed C-ABI parsers + the per-route frame.
2587
+ ///
2588
+ /// Calls the real `extern "C"` entry points (not just their cores) with
2589
+ /// arbitrary bytes and arbitrary output capacities: every call must return a
2590
+ /// value, never panic. This is the Rust-side half of the fuzz requirement
2591
+ /// (the JS-side harness lives in `scripts/`).
2592
+ #[test]
2593
+ fn packed_parsers_never_panic_on_hostile_c_abi_input() {
2594
+ use crate::test_support::Rng;
2595
+
2596
+ let mut rng = Rng(0xabad_1dea);
2597
+ let desc = route_desc_parse_both();
2598
+ let handle = unsafe { castrum_route_compile(desc.as_ptr(), desc.len()) };
2599
+
2600
+ for _ in 0..2_000 {
2601
+ let len = (rng.next() % 128) as usize;
2602
+ let data = rng.bytes(len);
2603
+ let mut out = vec![0u8; (rng.next() % 96) as usize];
2604
+ let (ptr, plen) = (data.as_ptr(), data.len());
2605
+ let (optr, ocap) = (out.as_mut_ptr(), out.len());
2606
+
2607
+ unsafe {
2608
+ let _ = castrum_http_parse_request_packed(ptr, plen, optr, ocap);
2609
+ let _ = castrum_query_parse_packed(ptr, plen, optr, ocap);
2610
+ let _ = castrum_cookie_parse_packed(ptr, plen, optr, ocap);
2611
+ let _ = castrum_form_parse_packed(ptr, plen, optr, ocap);
2612
+ let _ = castrum_multipart_parse_packed(ptr, plen, ptr, plen, optr, ocap);
2613
+ let _ = castrum_ws_frame_decode_packed(ptr, plen, optr, ocap);
2614
+ let _ = castrum_base64_decode(ptr, plen, optr, ocap, (rng.next() & 1) as u8, 1);
2615
+ let _ = castrum_hex_validate_batch(ptr, plen, (rng.next() % 40) as u32, optr, ocap);
2616
+ if handle != 0 {
2617
+ let _ = castrum_route_run(handle, ptr, plen, optr, ocap);
2618
+ }
2619
+ }
2620
+ }
2621
+
2622
+ if handle != 0 {
2623
+ unsafe { castrum_route_destroy(handle) };
2624
+ }
2625
+ }