reactor-effect-native 0.2.0 → 0.3.0-rc.0

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 (63) hide show
  1. package/Dockerfile +4 -2
  2. package/README.md +90 -15
  3. package/dist/_internal/bridge.d.ts +70 -14
  4. package/dist/_internal/bridge.d.ts.map +1 -1
  5. package/dist/_internal/bridge.js +290 -145
  6. package/dist/_internal/bridge.js.map +1 -1
  7. package/dist/_internal/isolated/child.d.ts +2 -0
  8. package/dist/_internal/isolated/child.d.ts.map +1 -0
  9. package/dist/_internal/isolated/child.js +176 -0
  10. package/dist/_internal/isolated/child.js.map +1 -0
  11. package/dist/_internal/isolated/host.d.ts +147 -0
  12. package/dist/_internal/isolated/host.d.ts.map +1 -0
  13. package/dist/_internal/isolated/host.js +645 -0
  14. package/dist/_internal/isolated/host.js.map +1 -0
  15. package/dist/_internal/isolated/protocol.d.ts +399 -0
  16. package/dist/_internal/isolated/protocol.d.ts.map +1 -0
  17. package/dist/_internal/isolated/protocol.js +250 -0
  18. package/dist/_internal/isolated/protocol.js.map +1 -0
  19. package/dist/_internal/peer.d.ts +68 -13
  20. package/dist/_internal/peer.d.ts.map +1 -1
  21. package/dist/_internal/peer.js +233 -157
  22. package/dist/_internal/peer.js.map +1 -1
  23. package/dist/index.d.ts +19 -10
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +37 -33
  26. package/dist/index.js.map +1 -1
  27. package/dist/isolated.d.ts +25 -0
  28. package/dist/isolated.d.ts.map +1 -0
  29. package/dist/isolated.js +39 -0
  30. package/dist/isolated.js.map +1 -0
  31. package/lib/darwin-arm64/libreactor_effect_native.dylib +0 -0
  32. package/lib/darwin-arm64/native-identity.json +4 -3
  33. package/lib/linux-x64/libreactor_effect_native.so +0 -0
  34. package/lib/linux-x64/native-identity.json +4 -3
  35. package/package.json +7 -3
  36. package/rust/Cargo.toml +108 -2
  37. package/rust/build.rs +253 -114
  38. package/rust/clippy.toml +7 -0
  39. package/rust/include/reactor_effect_native.h +101 -42
  40. package/rust/src/abi.rs +258 -0
  41. package/rust/src/error.rs +149 -0
  42. package/rust/src/ffi/memory.rs +256 -0
  43. package/rust/src/ffi/tests.rs +719 -0
  44. package/rust/src/ffi.rs +474 -0
  45. package/rust/src/lib.rs +36 -2341
  46. package/rust/src/peer/callbacks.rs +145 -0
  47. package/rust/src/peer/media.rs +221 -0
  48. package/rust/src/peer/owner/tests.rs +188 -0
  49. package/rust/src/peer/owner.rs +353 -0
  50. package/rust/src/peer/shared.rs +323 -0
  51. package/rust/src/peer/tests.rs +513 -0
  52. package/rust/src/peer.rs +189 -0
  53. package/rust/src/protocol/event.rs +238 -0
  54. package/rust/src/protocol/request.rs +347 -0
  55. package/rust/src/protocol/stats.rs +356 -0
  56. package/rust/src/protocol.rs +71 -0
  57. package/rust/src/sync/gate.rs +159 -0
  58. package/rust/src/sync/notifier.rs +136 -0
  59. package/rust/src/sync/queue.rs +384 -0
  60. package/rust/src/sync.rs +20 -0
  61. package/rust/src/test_support.rs +99 -0
  62. package/rust-toolchain.toml +7 -0
  63. package/scripts/stage.mjs +41 -1
@@ -0,0 +1,474 @@
1
+ //! The exported C ABI. `include/reactor_effect_native.h` is its contract.
2
+ //!
3
+ //! Every entry point catches panics, since unwinding into C is undefined: a
4
+ //! caught panic reports a native failure.
5
+ //!
6
+ //! A misaligned pointer argument is invalid input, and a length over its bound
7
+ //! is an overflow. Both are refused before any caller memory is touched, so
8
+ //! the `# Safety` sections below constrain only aligned pointers and lengths
9
+ //! within their bounds.
10
+
11
+ mod memory;
12
+ #[cfg(test)]
13
+ mod tests;
14
+
15
+ pub use crate::peer::ReactorEffectPeer;
16
+
17
+ use crate::abi::{
18
+ ABI_VERSION, CALL_BUFFER_MIN, FAILURE_MESSAGE_BYTES, MAX_MESSAGE_BYTES, MAX_REQUEST_BYTES,
19
+ Status,
20
+ };
21
+ use crate::error::{BridgeError, FailureClass};
22
+ use crate::sync::Taken;
23
+ use memory::{Out, OutSlice, input, peer_ref};
24
+ use std::ffi::{CStr, c_char};
25
+ use std::panic::{AssertUnwindSafe, catch_unwind};
26
+ use std::ptr;
27
+
28
+ /// Called on a peer's notifier thread with the readiness bits of every queue
29
+ /// that received an item since the previous call (`ReactorEffectNotify`).
30
+ pub type ReactorEffectNotify = extern "C" fn(ready: u32);
31
+
32
+ /// The header of one decoded BGRA frame, written by
33
+ /// [`reactor_effect_peer_take_video`].
34
+ #[repr(C)]
35
+ #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
36
+ pub struct ReactorEffectVideoHeader {
37
+ /// Frame width in pixels.
38
+ pub width: u32,
39
+ /// Frame height in pixels.
40
+ pub height: u32,
41
+ /// BGRA bytes: `width * height * 4`.
42
+ pub data_len: u32,
43
+ /// Bytes of the sender's frame-metadata user data.
44
+ pub metadata_len: u32,
45
+ /// The sender's frame ID; 0 when it supplied none.
46
+ pub frame_id: u64,
47
+ /// The sender's capture time in microseconds; 0 when absent.
48
+ pub timestamp_us: u64,
49
+ /// The track's index in the prepare request.
50
+ pub track: u32,
51
+ /// Always 0.
52
+ pub reserved: u32,
53
+ /// The frame's admission sequence on its track, from 0, stamped before
54
+ /// the queue can drop it: a gap is a frame the queue dropped.
55
+ pub sequence: u64,
56
+ }
57
+
58
+ /// The header of one interleaved PCM block, written by
59
+ /// [`reactor_effect_peer_take_audio`].
60
+ #[repr(C)]
61
+ #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
62
+ pub struct ReactorEffectAudioHeader {
63
+ /// Samples per second, per channel.
64
+ pub sample_rate: u32,
65
+ /// Interleaved channels.
66
+ pub channels: u32,
67
+ /// Interleaved `int16_t` samples: frames times channels.
68
+ pub samples: u32,
69
+ /// The track's index in the prepare request.
70
+ pub track: u32,
71
+ /// The block's admission sequence on its track, from 0, stamped before
72
+ /// the queue can drop it: a gap is a block the queue dropped.
73
+ pub sequence: u64,
74
+ }
75
+
76
+ /// Diagnostic text beside a failure status. Never match on it: the status is
77
+ /// the failure class.
78
+ #[repr(C)]
79
+ #[derive(Debug, Clone)]
80
+ pub struct ReactorEffectFailure {
81
+ /// Bytes of `message` in use.
82
+ pub message_len: u32,
83
+ /// UTF-8, truncated on a character boundary.
84
+ pub message: [u8; FAILURE_MESSAGE_BYTES],
85
+ }
86
+
87
+ impl ReactorEffectFailure {
88
+ /// The diagnostic for `message`, truncated to fit.
89
+ fn new(message: &str) -> Self {
90
+ let text = truncate(message, FAILURE_MESSAGE_BYTES).as_bytes();
91
+ let mut failure = Self {
92
+ message_len: 0,
93
+ message: [0; FAILURE_MESSAGE_BYTES],
94
+ };
95
+ // `truncate` keeps the text within the array, so this always copies.
96
+ if let (Some(prefix), Ok(len)) = (
97
+ failure.message.get_mut(..text.len()),
98
+ u32::try_from(text.len()),
99
+ ) {
100
+ prefix.copy_from_slice(text);
101
+ failure.message_len = len;
102
+ }
103
+ failure
104
+ }
105
+ }
106
+
107
+ /// The longest prefix of `text` within `max` bytes that ends on a character
108
+ /// boundary.
109
+ fn truncate(text: &str, max: usize) -> &str {
110
+ (0..=max.min(text.len()))
111
+ .rev()
112
+ .find_map(|end| text.get(..end))
113
+ .unwrap_or_default()
114
+ }
115
+
116
+ /// This image's source and build identity: a NUL-terminated C string inside a
117
+ /// marker that packaging finds by scanning the file, without loading a
118
+ /// foreign-platform library.
119
+ static BUILD_IDENTITY: &CStr = {
120
+ let marked = concat!(
121
+ "reactor-effect-native:build-identity:",
122
+ env!("REACTOR_EFFECT_BUILD_IDENTITY"),
123
+ ":end\0"
124
+ );
125
+ match CStr::from_bytes_with_nul(marked.as_bytes()) {
126
+ Ok(identity) => identity,
127
+ Err(_) => panic!("the build identity must be one NUL-terminated C string"),
128
+ }
129
+ };
130
+
131
+ /// The ABI version this library implements. The host refuses any other.
132
+ #[unsafe(no_mangle)]
133
+ pub extern "C" fn reactor_effect_abi_version() -> u32 {
134
+ ABI_VERSION
135
+ }
136
+
137
+ /// The static source and build identity of this loaded image. Never free it.
138
+ #[unsafe(no_mangle)]
139
+ pub extern "C" fn reactor_effect_build_identity() -> *const c_char {
140
+ BUILD_IDENTITY.as_ptr()
141
+ }
142
+
143
+ /// Allocate a peer, or return null when its threads cannot start.
144
+ ///
145
+ /// When `notify` is non-null, a notifier thread calls it with the readiness
146
+ /// bits of the queues that received items since its previous call, until
147
+ /// shutdown joins that thread.
148
+ #[unsafe(no_mangle)]
149
+ pub extern "C" fn reactor_effect_peer_create(
150
+ notify: Option<ReactorEffectNotify>,
151
+ ) -> *mut ReactorEffectPeer {
152
+ catch_unwind(|| ReactorEffectPeer::create(notify))
153
+ .ok()
154
+ .flatten()
155
+ .map_or(ptr::null_mut(), |peer| Box::into_raw(Box::new(peer)))
156
+ }
157
+
158
+ /// Run one serialized peer operation. `request` is UTF-8; on success the
159
+ /// response is UTF-8 JSON and `response_cap` must be at least 4 MiB, which
160
+ /// `BUFFER_TOO_SMALL` reports through `response_len`.
161
+ ///
162
+ /// # Safety
163
+ /// `peer` must be null or a live handle from [`reactor_effect_peer_create`].
164
+ /// A nonzero `request_len` of at most 1 MiB needs `request` valid for reads of
165
+ /// that many bytes. A non-null `response` must be valid for writes of
166
+ /// `response_cap` bytes, a non-null `response_len` for one `usize`, and a
167
+ /// non-null `failure` for one [`ReactorEffectFailure`].
168
+ #[unsafe(no_mangle)]
169
+ pub unsafe extern "C" fn reactor_effect_peer_call(
170
+ peer: *mut ReactorEffectPeer,
171
+ operation: u32,
172
+ request: *const u8,
173
+ request_len: usize,
174
+ response: *mut u8,
175
+ response_cap: usize,
176
+ response_len: *mut usize,
177
+ failure: *mut ReactorEffectFailure,
178
+ ) -> i32 {
179
+ // SAFETY: a non-null `failure` is writable for the call, per the contract.
180
+ let failure = unsafe { Out::new(failure) };
181
+ // SAFETY: a non-null `response_len` is writable for the call.
182
+ let response_len = unsafe { Out::new(response_len) };
183
+ // SAFETY: a non-null `response` is writable for `response_cap` bytes.
184
+ let response = unsafe { OutSlice::new(response, response_cap) };
185
+ status_of(failure, || {
186
+ // SAFETY: `peer` is null or live for the call.
187
+ let peer = unsafe { peer_ref(peer) }?;
188
+ let mut response = response?;
189
+ let Some(mut response_len) = response_len?.filter(|_| !response.is_null()) else {
190
+ return Err(BridgeError::invalid("call requires a response buffer"));
191
+ };
192
+ response_len.write(0);
193
+ if response_cap < CALL_BUFFER_MIN {
194
+ response_len.write(CALL_BUFFER_MIN);
195
+ return Ok(Status::BufferTooSmall);
196
+ }
197
+ // SAFETY: a nonzero `request_len` within its bound makes `request`
198
+ // readable for it.
199
+ let request = unsafe { input(request, request_len, MAX_REQUEST_BYTES) }?;
200
+ let bytes = peer.call(operation, request)?;
201
+ if !response.holds(bytes.len()) {
202
+ return Err(BridgeError::overflow(
203
+ "native call response exceeds its buffer",
204
+ ));
205
+ }
206
+ response.copy_from(&bytes)?;
207
+ response_len.write(bytes.len());
208
+ Ok(Status::Ok)
209
+ })
210
+ }
211
+
212
+ /// Send one binary SCTP message on a bridge-owned data channel.
213
+ ///
214
+ /// # Safety
215
+ /// `peer` must be null or a live handle from [`reactor_effect_peer_create`].
216
+ /// A nonzero `data_len` of at most 256 KiB needs `data` valid for reads of
217
+ /// that many bytes, and a non-null `failure` must be writable for one
218
+ /// [`ReactorEffectFailure`].
219
+ #[unsafe(no_mangle)]
220
+ pub unsafe extern "C" fn reactor_effect_peer_send(
221
+ peer: *mut ReactorEffectPeer,
222
+ channel: u32,
223
+ data: *const u8,
224
+ data_len: usize,
225
+ failure: *mut ReactorEffectFailure,
226
+ ) -> i32 {
227
+ // SAFETY: a non-null `failure` is writable for the call.
228
+ let failure = unsafe { Out::new(failure) };
229
+ status_of(failure, || {
230
+ // SAFETY: `peer` is null or live for the call.
231
+ let peer = unsafe { peer_ref(peer) }?;
232
+ // SAFETY: a nonzero `data_len` within its bound makes `data` readable
233
+ // for it.
234
+ let bytes = unsafe { input(data, data_len, MAX_MESSAGE_BYTES) }?;
235
+ peer.send(channel, bytes)?;
236
+ Ok(Status::Ok)
237
+ })
238
+ }
239
+
240
+ /// Nonblocking: copy the oldest transport event into caller memory and
241
+ /// remove it. `BUFFER_TOO_SMALL` writes the required size to `out_len` and
242
+ /// keeps the event queued; `AGAIN` means the queue is empty.
243
+ ///
244
+ /// # Safety
245
+ /// `peer` must be null or a live handle from [`reactor_effect_peer_create`].
246
+ /// A non-null `out_len` must be writable for one `usize`, and a nonzero
247
+ /// `out_cap` needs `out` valid for writes of that many bytes.
248
+ #[unsafe(no_mangle)]
249
+ pub unsafe extern "C" fn reactor_effect_peer_take_event(
250
+ peer: *mut ReactorEffectPeer,
251
+ out: *mut u8,
252
+ out_cap: usize,
253
+ out_len: *mut usize,
254
+ ) -> i32 {
255
+ // SAFETY: a non-null `out_len` is writable for the call.
256
+ let out_len = unsafe { Out::new(out_len) };
257
+ // SAFETY: a nonzero `out_cap` makes `out` writable for it; `holds` never
258
+ // accepts bytes for a null `out`.
259
+ let out = unsafe { OutSlice::new(out, out_cap) };
260
+ status_of(Ok(None), || {
261
+ // SAFETY: `peer` is null or live for the call.
262
+ let peer = unsafe { peer_ref(peer) }?;
263
+ let mut out = out?;
264
+ let Some(mut out_len) = out_len? else {
265
+ return Err(BridgeError::invalid("take_event requires out_len"));
266
+ };
267
+ if out_cap != 0 && out.is_null() {
268
+ return Err(BridgeError::invalid("null event buffer with a capacity"));
269
+ }
270
+ let taken = peer.shared().events.take(|packet| {
271
+ out_len.write(packet.len());
272
+ out.holds(packet.len())
273
+ });
274
+ Ok(match taken {
275
+ Taken::Item(packet) => {
276
+ out.copy_from(&packet)?;
277
+ Status::Ok
278
+ }
279
+ Taken::TooSmall => Status::BufferTooSmall,
280
+ Taken::Empty => {
281
+ out_len.write(0);
282
+ Status::Again
283
+ }
284
+ Taken::Closed => {
285
+ out_len.write(0);
286
+ Status::Closed
287
+ }
288
+ })
289
+ })
290
+ }
291
+
292
+ /// Nonblocking: copy the oldest decoded frame into caller memory and remove it.
293
+ ///
294
+ /// The header is written for `OK` and for `BUFFER_TOO_SMALL`, which keeps the
295
+ /// frame queued; a null `bgra` asks for the header alone.
296
+ ///
297
+ /// # Safety
298
+ /// `peer` must be null or a live handle from [`reactor_effect_peer_create`].
299
+ /// A non-null `header` must be writable for one [`ReactorEffectVideoHeader`],
300
+ /// a non-null `bgra` for `bgra_cap` bytes and a non-null `metadata` for
301
+ /// `metadata_cap` bytes.
302
+ #[unsafe(no_mangle)]
303
+ pub unsafe extern "C" fn reactor_effect_peer_take_video(
304
+ peer: *mut ReactorEffectPeer,
305
+ header: *mut ReactorEffectVideoHeader,
306
+ bgra: *mut u8,
307
+ bgra_cap: usize,
308
+ metadata: *mut u8,
309
+ metadata_cap: usize,
310
+ ) -> i32 {
311
+ // SAFETY: a non-null `header` is writable for the call.
312
+ let header = unsafe { Out::new(header) };
313
+ // SAFETY: a non-null `bgra` is writable for `bgra_cap` bytes.
314
+ let bgra = unsafe { OutSlice::new(bgra, bgra_cap) };
315
+ // SAFETY: a non-null `metadata` is writable for `metadata_cap` bytes.
316
+ let metadata = unsafe { OutSlice::new(metadata, metadata_cap) };
317
+ status_of(Ok(None), || {
318
+ // SAFETY: `peer` is null or live for the call.
319
+ let peer = unsafe { peer_ref(peer) }?;
320
+ let (mut bgra, mut metadata) = (bgra?, metadata?);
321
+ let Some(mut header) = header? else {
322
+ return Err(BridgeError::invalid("take_video requires a header"));
323
+ };
324
+ let taken = peer.shared().video.take(|frame| {
325
+ header.write(frame.header());
326
+ !bgra.is_null() && bgra.holds(frame.bgra.len()) && metadata.holds(frame.metadata.len())
327
+ });
328
+ Ok(match taken {
329
+ Taken::Item(frame) => {
330
+ bgra.copy_from(&frame.bgra)?;
331
+ metadata.copy_from(&frame.metadata)?;
332
+ Status::Ok
333
+ }
334
+ Taken::TooSmall => Status::BufferTooSmall,
335
+ Taken::Empty => Status::Again,
336
+ Taken::Closed => Status::Closed,
337
+ })
338
+ })
339
+ }
340
+
341
+ /// Nonblocking: copy the oldest PCM block into caller memory and remove it.
342
+ ///
343
+ /// The block is native-endian interleaved `int16_t`. The header is written
344
+ /// for `OK` and for `BUFFER_TOO_SMALL`; a null `pcm` asks for the header alone.
345
+ ///
346
+ /// # Safety
347
+ /// `peer` must be null or a live handle from [`reactor_effect_peer_create`].
348
+ /// A non-null `header` must be writable for one [`ReactorEffectAudioHeader`],
349
+ /// and a non-null `pcm` for `pcm_cap` samples.
350
+ #[unsafe(no_mangle)]
351
+ pub unsafe extern "C" fn reactor_effect_peer_take_audio(
352
+ peer: *mut ReactorEffectPeer,
353
+ header: *mut ReactorEffectAudioHeader,
354
+ pcm: *mut i16,
355
+ pcm_cap: usize,
356
+ ) -> i32 {
357
+ // SAFETY: a non-null `header` is writable for the call.
358
+ let header = unsafe { Out::new(header) };
359
+ // SAFETY: a non-null `pcm` is writable for `pcm_cap` samples.
360
+ let pcm = unsafe { OutSlice::new(pcm, pcm_cap) };
361
+ status_of(Ok(None), || {
362
+ // SAFETY: `peer` is null or live for the call.
363
+ let peer = unsafe { peer_ref(peer) }?;
364
+ let mut pcm = pcm?;
365
+ let Some(mut header) = header? else {
366
+ return Err(BridgeError::invalid("take_audio requires a header"));
367
+ };
368
+ let taken = peer.shared().audio.take(|block| {
369
+ header.write(block.header());
370
+ !pcm.is_null() && pcm.holds(block.pcm.len())
371
+ });
372
+ Ok(match taken {
373
+ Taken::Item(block) => {
374
+ pcm.copy_from(&block.pcm)?;
375
+ Status::Ok
376
+ }
377
+ Taken::TooSmall => Status::BufferTooSmall,
378
+ Taken::Empty => Status::Again,
379
+ Taken::Closed => Status::Closed,
380
+ })
381
+ })
382
+ }
383
+
384
+ /// Fence callback and event admission at once. No callback or event is
385
+ /// admitted after it returns.
386
+ ///
387
+ /// # Safety
388
+ /// `peer` must be null or a live handle from [`reactor_effect_peer_create`].
389
+ #[unsafe(no_mangle)]
390
+ pub unsafe extern "C" fn reactor_effect_peer_close(peer: *mut ReactorEffectPeer) {
391
+ // SAFETY: `peer` is null or live for the call.
392
+ if let Ok(peer) = unsafe { peer_ref(peer) } {
393
+ // Closing only stores flags and clears queues.
394
+ discard_panic(|| peer.close());
395
+ }
396
+ }
397
+
398
+ /// Join native ownership: close, then drop the libwebrtc objects on the owner
399
+ /// thread, wait for every admitted callback, and join the owner and notifier
400
+ /// threads. Safe to call more than once.
401
+ ///
402
+ /// # Safety
403
+ /// `peer` must be null or a live handle from [`reactor_effect_peer_create`],
404
+ /// and a non-null `failure` must be writable for one [`ReactorEffectFailure`].
405
+ /// The notifier may be waiting for the host to run its callback, so never
406
+ /// call this from the thread that runs that callback.
407
+ #[unsafe(no_mangle)]
408
+ pub unsafe extern "C" fn reactor_effect_peer_shutdown(
409
+ peer: *mut ReactorEffectPeer,
410
+ failure: *mut ReactorEffectFailure,
411
+ ) -> i32 {
412
+ // SAFETY: a non-null `failure` is writable for the call.
413
+ let failure = unsafe { Out::new(failure) };
414
+ status_of(failure, || {
415
+ // SAFETY: `peer` is null or live for the call.
416
+ unsafe { peer_ref(peer) }?.shutdown()?;
417
+ Ok(Status::Ok)
418
+ })
419
+ }
420
+
421
+ /// Shut a peer down if needed, so the same thread rule applies, then free its
422
+ /// handle.
423
+ ///
424
+ /// # Safety
425
+ /// `peer` must be null or a handle from [`reactor_effect_peer_create`] not
426
+ /// passed here before. Every foreign call using the handle must have
427
+ /// returned, including calls still queued in a host FFI executor: joining the
428
+ /// native owner alone does not establish that.
429
+ #[unsafe(no_mangle)]
430
+ pub unsafe extern "C" fn reactor_effect_peer_destroy(peer: *mut ReactorEffectPeer) {
431
+ // A misaligned pointer never came from `reactor_effect_peer_create`, and
432
+ // this call has no status to refuse it with.
433
+ if peer.is_null() || !peer.is_aligned() {
434
+ return;
435
+ }
436
+ // SAFETY: `peer` came from `Box::into_raw` in `reactor_effect_peer_create`,
437
+ // is passed here once, and no other call still uses it.
438
+ let peer = unsafe { Box::from_raw(peer) };
439
+ // Dropping shuts the peer down.
440
+ discard_panic(move || drop(peer));
441
+ }
442
+
443
+ /// Run the body of an entry point that returns no status: a panic there has
444
+ /// nothing to report it and must not unwind into C.
445
+ fn discard_panic(body: impl FnOnce()) {
446
+ #[expect(
447
+ clippy::let_underscore_must_use,
448
+ reason = "the entry point has no status to report a caught panic with"
449
+ )]
450
+ let _ = catch_unwind(AssertUnwindSafe(body));
451
+ }
452
+
453
+ /// The status of an entry point's body. A failure's diagnostic goes to
454
+ /// `failure` when the caller passed one, and a caught panic is a native
455
+ /// failure. A misaligned `failure` fails the call before the body runs, since
456
+ /// it cannot hold its own diagnostic.
457
+ fn status_of(
458
+ failure: Result<Option<Out<ReactorEffectFailure>>, BridgeError>,
459
+ body: impl FnOnce() -> Result<Status, BridgeError>,
460
+ ) -> i32 {
461
+ let failure = match failure {
462
+ Ok(failure) => failure,
463
+ Err(misaligned) => return misaligned.class.status().code(),
464
+ };
465
+ let error = match catch_unwind(AssertUnwindSafe(body)) {
466
+ Ok(Ok(status)) => return status.code(),
467
+ Ok(Err(error)) => error,
468
+ Err(_) => BridgeError::new(FailureClass::Native, "native bridge panicked"),
469
+ };
470
+ if let Some(mut failure) = failure {
471
+ failure.write(ReactorEffectFailure::new(&error.message));
472
+ }
473
+ error.class.status().code()
474
+ }