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,258 @@
1
+ //! The constants of the C ABI, mirrored from `include/reactor_effect_native.h`.
2
+ //!
3
+ //! A test parses the header and fails when the two disagree. The host mirrors
4
+ //! them too, in `packages/native/src/_internal/bridge.ts`.
5
+
6
+ use crate::error::BridgeError;
7
+ use serde::{Serialize, Serializer};
8
+
9
+ /// The ABI version the header declares. The host refuses any other.
10
+ pub(crate) const ABI_VERSION: u32 = 4;
11
+
12
+ /// The capacity of `ReactorEffectFailure::message`, in bytes.
13
+ pub(crate) const FAILURE_MESSAGE_BYTES: usize = 1020;
14
+
15
+ /// The smallest response buffer `reactor_effect_peer_call` accepts.
16
+ pub(crate) const CALL_BUFFER_MIN: usize = 4 * 1024 * 1024;
17
+
18
+ /// The largest request `reactor_effect_peer_call` accepts.
19
+ pub(crate) const MAX_REQUEST_BYTES: usize = 1024 * 1024;
20
+
21
+ /// The largest data channel message, in either direction.
22
+ pub(crate) const MAX_MESSAGE_BYTES: usize = 256 * 1024;
23
+
24
+ /// The most a data channel may hold unsent before a send is refused.
25
+ pub(crate) const MAX_BUFFERED_SEND_BYTES: u64 = 1024 * 1024;
26
+
27
+ /// What an entry point reports (`enum ReactorEffectStatus`).
28
+ ///
29
+ /// Non-negative statuses are outcomes. Negative statuses are failure classes,
30
+ /// closed for this ABI; the host maps each one to its own error type.
31
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
32
+ #[repr(i32)]
33
+ pub(crate) enum Status {
34
+ Ok = 0,
35
+ /// A take found its queue empty.
36
+ Again = 1,
37
+ /// The required sizes were written and the item stays queued.
38
+ BufferTooSmall = 2,
39
+ /// The peer is fenced or shut down.
40
+ Closed = 3,
41
+ /// An argument or request was rejected.
42
+ InvalidInput = -1,
43
+ /// libwebrtc or the bridge failed in a way it cannot classify.
44
+ Native = -2,
45
+ /// A queue, buffer or message bound was exceeded.
46
+ Overflow = -3,
47
+ /// The remote peer broke the negotiated contract.
48
+ Protocol = -4,
49
+ /// libwebrtc refused to create or apply an SDP.
50
+ SdpRejected = -5,
51
+ /// The data channel is not open.
52
+ ChannelClosed = -6,
53
+ }
54
+
55
+ impl Status {
56
+ /// The C `int` an entry point returns for this status.
57
+ pub(crate) const fn code(self) -> i32 {
58
+ self as i32
59
+ }
60
+ }
61
+
62
+ /// A peer operation run by `reactor_effect_peer_call` (`enum ReactorEffectCall`).
63
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
64
+ pub(crate) enum Operation {
65
+ /// Create the connection and its local offer.
66
+ Prepare = 1,
67
+ /// Apply the remote answer.
68
+ Answer = 2,
69
+ /// Pause or resume a declared track.
70
+ Direction = 3,
71
+ /// Cap an outgoing track's send bitrate.
72
+ MaxBitrate = 4,
73
+ /// Read WebRTC statistics.
74
+ Stats = 5,
75
+ /// Read queue pressure.
76
+ MediaSnapshot = 6,
77
+ }
78
+
79
+ impl Operation {
80
+ const ALL: [Self; 6] = [
81
+ Self::Prepare,
82
+ Self::Answer,
83
+ Self::Direction,
84
+ Self::MaxBitrate,
85
+ Self::Stats,
86
+ Self::MediaSnapshot,
87
+ ];
88
+ }
89
+
90
+ impl TryFrom<u32> for Operation {
91
+ type Error = BridgeError;
92
+
93
+ fn try_from(code: u32) -> Result<Self, BridgeError> {
94
+ Self::ALL
95
+ .into_iter()
96
+ .find(|operation| *operation as u32 == code)
97
+ .ok_or_else(|| BridgeError::invalid("unknown native call operation"))
98
+ }
99
+ }
100
+
101
+ /// A data channel the bridge owns (`enum ReactorEffectChannel`).
102
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
103
+ pub(crate) enum Channel {
104
+ /// Carries Reactor control messages.
105
+ Control = 0,
106
+ /// Carries Reactor data messages.
107
+ Data = 1,
108
+ }
109
+
110
+ impl Channel {
111
+ const ALL: [Self; 2] = [Self::Control, Self::Data];
112
+
113
+ /// The channel's SCTP label, which also names it in event headers.
114
+ pub(crate) const fn label(self) -> &'static str {
115
+ match self {
116
+ Self::Control => "control",
117
+ Self::Data => "data",
118
+ }
119
+ }
120
+ }
121
+
122
+ impl TryFrom<u32> for Channel {
123
+ type Error = BridgeError;
124
+
125
+ fn try_from(code: u32) -> Result<Self, BridgeError> {
126
+ Self::ALL
127
+ .into_iter()
128
+ .find(|channel| *channel as u32 == code)
129
+ .ok_or_else(|| BridgeError::invalid("unknown data channel"))
130
+ }
131
+ }
132
+
133
+ impl Serialize for Channel {
134
+ fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
135
+ serializer.serialize_str(self.label())
136
+ }
137
+ }
138
+
139
+ /// A readiness bit passed to the host's notify callback (`enum ReactorEffectReady`).
140
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
141
+ pub(crate) enum Ready {
142
+ /// The transport event queue received an event.
143
+ Events = 1,
144
+ /// The decoded video queue received a frame.
145
+ Video = 2,
146
+ /// The decoded audio queue received a block.
147
+ Audio = 4,
148
+ }
149
+
150
+ impl Ready {
151
+ /// This readiness as its bit of the callback's mask.
152
+ pub(crate) const fn bit(self) -> u32 {
153
+ self as u32
154
+ }
155
+ }
156
+
157
+ #[cfg(test)]
158
+ mod tests {
159
+ use super::*;
160
+ use std::collections::BTreeMap;
161
+
162
+ const HEADER: &str = include_str!("../include/reactor_effect_native.h");
163
+
164
+ /// Every `REACTOR_EFFECT_<NAME> = <value>` enumerator the header declares.
165
+ fn header_enumerators() -> BTreeMap<String, i64> {
166
+ HEADER
167
+ .lines()
168
+ .filter_map(|line| {
169
+ let (name, value) = line
170
+ .trim()
171
+ .strip_prefix("REACTOR_EFFECT_")?
172
+ .split_once(" = ")?;
173
+ let digits: String = value
174
+ .chars()
175
+ .take_while(|c| *c == '-' || c.is_ascii_digit())
176
+ .collect();
177
+ Some((name.to_owned(), digits.parse().ok()?))
178
+ })
179
+ .collect()
180
+ }
181
+
182
+ #[test]
183
+ fn the_header_declares_exactly_these_statuses_calls_channels_and_readiness_bits() {
184
+ let statuses = [
185
+ ("OK", Status::Ok),
186
+ ("AGAIN", Status::Again),
187
+ ("BUFFER_TOO_SMALL", Status::BufferTooSmall),
188
+ ("CLOSED", Status::Closed),
189
+ ("INVALID_INPUT", Status::InvalidInput),
190
+ ("NATIVE", Status::Native),
191
+ ("OVERFLOW", Status::Overflow),
192
+ ("PROTOCOL", Status::Protocol),
193
+ ("SDP_REJECTED", Status::SdpRejected),
194
+ ("CHANNEL_CLOSED", Status::ChannelClosed),
195
+ ]
196
+ .map(|(name, status)| (name, i64::from(status.code())));
197
+ let calls = [
198
+ ("PREPARE", Operation::Prepare),
199
+ ("ANSWER", Operation::Answer),
200
+ ("DIRECTION", Operation::Direction),
201
+ ("MAX_BITRATE", Operation::MaxBitrate),
202
+ ("STATS", Operation::Stats),
203
+ ("MEDIA_SNAPSHOT", Operation::MediaSnapshot),
204
+ ]
205
+ .map(|(name, operation)| (name, i64::from(operation as u32)));
206
+ let channels = [("CONTROL", Channel::Control), ("DATA", Channel::Data)]
207
+ .map(|(name, channel)| (name, i64::from(channel as u32)));
208
+ let ready = [
209
+ ("READY_EVENTS", Ready::Events),
210
+ ("READY_VIDEO", Ready::Video),
211
+ ("READY_AUDIO", Ready::Audio),
212
+ ]
213
+ .map(|(name, ready)| (name, i64::from(ready.bit())));
214
+
215
+ let expected: BTreeMap<String, i64> = statuses
216
+ .into_iter()
217
+ .chain(calls)
218
+ .chain(channels)
219
+ .chain(ready)
220
+ .map(|(name, value)| (name.to_owned(), value))
221
+ .collect();
222
+ assert_eq!(header_enumerators(), expected);
223
+ }
224
+
225
+ #[test]
226
+ fn the_header_declares_this_abi_version_and_failure_capacity() {
227
+ assert!(HEADER.contains(&format!("/* ABI {ABI_VERSION}.")));
228
+ assert!(HEADER.contains(&format!("uint8_t message[{FAILURE_MESSAGE_BYTES}];")));
229
+ }
230
+
231
+ #[test]
232
+ fn every_call_and_channel_code_parses_back_to_its_variant() {
233
+ for operation in Operation::ALL {
234
+ assert_eq!(Operation::try_from(operation as u32), Ok(operation));
235
+ }
236
+ for channel in Channel::ALL {
237
+ assert_eq!(Channel::try_from(channel as u32), Ok(channel));
238
+ }
239
+ }
240
+
241
+ #[test]
242
+ fn unknown_call_and_channel_codes_are_invalid_input() {
243
+ for code in [0, 7, u32::MAX] {
244
+ let error = Operation::try_from(code).unwrap_err();
245
+ assert_eq!(error, BridgeError::invalid("unknown native call operation"));
246
+ }
247
+ for code in [2, u32::MAX] {
248
+ let error = Channel::try_from(code).unwrap_err();
249
+ assert_eq!(error, BridgeError::invalid("unknown data channel"));
250
+ }
251
+ }
252
+
253
+ #[test]
254
+ fn channels_serialize_as_their_labels() {
255
+ let labels = serde_json::to_value(Channel::ALL).unwrap();
256
+ assert_eq!(labels, serde_json::json!(["control", "data"]));
257
+ }
258
+ }
@@ -0,0 +1,149 @@
1
+ //! Bridge failures and the ABI failure class each one reports.
2
+
3
+ use crate::abi::Status;
4
+ use std::fmt;
5
+ use std::sync::mpsc::{RecvError, SendError};
6
+
7
+ /// The failure class of a [`BridgeError`], which fixes the status the C ABI
8
+ /// reports for it.
9
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
10
+ pub(crate) enum FailureClass {
11
+ Closed,
12
+ InvalidInput,
13
+ Native,
14
+ Overflow,
15
+ Protocol,
16
+ SdpRejected,
17
+ ChannelClosed,
18
+ }
19
+
20
+ impl FailureClass {
21
+ /// The status an entry point returns for this class.
22
+ pub(crate) const fn status(self) -> Status {
23
+ match self {
24
+ Self::Closed => Status::Closed,
25
+ Self::InvalidInput => Status::InvalidInput,
26
+ Self::Native => Status::Native,
27
+ Self::Overflow => Status::Overflow,
28
+ Self::Protocol => Status::Protocol,
29
+ Self::SdpRejected => Status::SdpRejected,
30
+ Self::ChannelClosed => Status::ChannelClosed,
31
+ }
32
+ }
33
+ }
34
+
35
+ /// A classified failure. Its message is diagnostic text for the host, which
36
+ /// never matches on it: the class is the failure.
37
+ #[derive(Debug, Clone, PartialEq, Eq)]
38
+ pub(crate) struct BridgeError {
39
+ pub(crate) class: FailureClass,
40
+ pub(crate) message: String,
41
+ }
42
+
43
+ impl BridgeError {
44
+ pub(crate) fn new(class: FailureClass, message: impl Into<String>) -> Self {
45
+ Self {
46
+ class,
47
+ message: message.into(),
48
+ }
49
+ }
50
+
51
+ pub(crate) fn invalid(message: impl Into<String>) -> Self {
52
+ Self::new(FailureClass::InvalidInput, message)
53
+ }
54
+
55
+ pub(crate) fn overflow(message: impl Into<String>) -> Self {
56
+ Self::new(FailureClass::Overflow, message)
57
+ }
58
+
59
+ pub(crate) fn closed() -> Self {
60
+ Self::new(FailureClass::Closed, "native peer is closed")
61
+ }
62
+ }
63
+
64
+ impl fmt::Display for BridgeError {
65
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66
+ f.write_str(&self.message)
67
+ }
68
+ }
69
+
70
+ impl std::error::Error for BridgeError {}
71
+
72
+ /// A peer's channels to its owner thread disconnect only once that thread has
73
+ /// stopped, so a failed send or receive means the peer has closed.
74
+ impl<T> From<SendError<T>> for BridgeError {
75
+ fn from(_: SendError<T>) -> Self {
76
+ Self::closed()
77
+ }
78
+ }
79
+
80
+ impl From<RecvError> for BridgeError {
81
+ fn from(_: RecvError) -> Self {
82
+ Self::closed()
83
+ }
84
+ }
85
+
86
+ /// Classifies reactor-webrtc failures.
87
+ ///
88
+ /// reactor-webrtc reports every libwebrtc failure as an untyped string, so the
89
+ /// class comes from the operation that failed rather than from the error.
90
+ pub(crate) trait Classify<T> {
91
+ /// Report a failure of `operation` as `class`, keeping libwebrtc's text.
92
+ fn classify(self, class: FailureClass, operation: &str) -> Result<T, BridgeError>;
93
+ }
94
+
95
+ impl<T> Classify<T> for reactor_webrtc::Result<T> {
96
+ fn classify(self, class: FailureClass, operation: &str) -> Result<T, BridgeError> {
97
+ self.map_err(|error| BridgeError::new(class, format!("{operation}: {error}")))
98
+ }
99
+ }
100
+
101
+ #[cfg(test)]
102
+ mod tests {
103
+ use super::*;
104
+ use std::sync::mpsc;
105
+
106
+ #[test]
107
+ fn a_classified_failure_names_its_operation_and_keeps_the_libwebrtc_text() {
108
+ let failed: reactor_webrtc::Result<()> =
109
+ Err(reactor_webrtc::Error::Webrtc("bad fingerprint".into()));
110
+ let error = failed
111
+ .classify(FailureClass::SdpRejected, "set_remote_description")
112
+ .unwrap_err();
113
+ assert_eq!(error.class, FailureClass::SdpRejected);
114
+ assert_eq!(
115
+ error.message,
116
+ "set_remote_description: webrtc error: bad fingerprint"
117
+ );
118
+ }
119
+
120
+ #[test]
121
+ fn every_failure_class_reports_a_failure_status_except_closed() {
122
+ let classes = [
123
+ FailureClass::InvalidInput,
124
+ FailureClass::Native,
125
+ FailureClass::Overflow,
126
+ FailureClass::Protocol,
127
+ FailureClass::SdpRejected,
128
+ FailureClass::ChannelClosed,
129
+ ];
130
+ for class in classes {
131
+ assert!(class.status().code() < 0, "{class:?}");
132
+ }
133
+ // Closed is an outcome the host expects, not a failure of the call.
134
+ assert_eq!(FailureClass::Closed.status(), Status::Closed);
135
+ }
136
+
137
+ #[test]
138
+ fn a_disconnected_owner_channel_reads_as_a_closed_peer() {
139
+ let (sender, receiver) = mpsc::channel::<()>();
140
+ drop(receiver);
141
+ let unsent = sender.send(()).unwrap_err();
142
+ assert_eq!(BridgeError::from(unsent), BridgeError::closed());
143
+
144
+ let (sender, receiver) = mpsc::channel::<()>();
145
+ drop(sender);
146
+ let unreceived = receiver.recv().unwrap_err();
147
+ assert_eq!(BridgeError::from(unreceived), BridgeError::closed());
148
+ }
149
+ }
@@ -0,0 +1,256 @@
1
+ //! Views of caller memory.
2
+ //!
3
+ //! An entry point creates each view, unsafely, from one pointer argument
4
+ //! under the clause of its contract that covers that pointer. Using a view is
5
+ //! then safe. Output views write through raw pointers and never form
6
+ //! references, since caller memory may be uninitialized.
7
+ //!
8
+ //! A misaligned pointer, or a length over its bound, is refused before any
9
+ //! caller memory is touched: no valid caller passes one, and using it would be
10
+ //! undefined behavior.
11
+
12
+ use crate::error::{BridgeError, FailureClass};
13
+ use crate::peer::ReactorEffectPeer;
14
+ use std::any::type_name;
15
+ use std::ptr::{self, NonNull};
16
+ use std::slice;
17
+
18
+ /// Caller memory for one `T`.
19
+ pub(super) struct Out<T>(NonNull<T>);
20
+
21
+ impl<T> Out<T> {
22
+ /// A view of `ptr`, or `None` when it is null.
23
+ ///
24
+ /// # Errors
25
+ /// Invalid input when `ptr` is not aligned for `T`.
26
+ ///
27
+ /// # Safety
28
+ /// A non-null, aligned `ptr` must be valid for writes of one `T` until the
29
+ /// entry point returns.
30
+ pub(super) unsafe fn new(ptr: *mut T) -> Result<Option<Self>, BridgeError> {
31
+ aligned(ptr)?;
32
+ Ok(NonNull::new(ptr).map(Self))
33
+ }
34
+
35
+ pub(super) fn write(&mut self, value: T) {
36
+ // SAFETY: `new`'s caller made the location writable until the entry
37
+ // point returns, and views never outlive their entry point.
38
+ unsafe { self.0.as_ptr().write(value) }
39
+ }
40
+ }
41
+
42
+ /// Caller memory for up to `capacity` elements. It may be null when the
43
+ /// caller only asks for the sizes of what is queued.
44
+ pub(super) struct OutSlice<T> {
45
+ ptr: *mut T,
46
+ capacity: usize,
47
+ }
48
+
49
+ impl<T: Copy> OutSlice<T> {
50
+ /// A view of `capacity` elements at `ptr`, which may be null.
51
+ ///
52
+ /// # Errors
53
+ /// Invalid input when `ptr` is not aligned for `T`.
54
+ ///
55
+ /// # Safety
56
+ /// A non-null, aligned `ptr` must be valid for writes of `capacity`
57
+ /// elements until the entry point returns.
58
+ pub(super) unsafe fn new(ptr: *mut T, capacity: usize) -> Result<Self, BridgeError> {
59
+ aligned(ptr)?;
60
+ Ok(Self { ptr, capacity })
61
+ }
62
+
63
+ pub(super) fn is_null(&self) -> bool {
64
+ self.ptr.is_null()
65
+ }
66
+
67
+ /// Whether `len` elements fit.
68
+ pub(super) fn holds(&self, len: usize) -> bool {
69
+ len <= self.capacity && (len == 0 || !self.ptr.is_null())
70
+ }
71
+
72
+ /// Copy `items` to the start of the buffer.
73
+ ///
74
+ /// # Errors
75
+ /// A native failure, copying nothing, if the buffer cannot hold them.
76
+ /// Callers rule that out with [`holds`](Self::holds) first.
77
+ pub(super) fn copy_from(&mut self, items: &[T]) -> Result<(), BridgeError> {
78
+ if !self.holds(items.len()) {
79
+ return Err(BridgeError::new(
80
+ FailureClass::Native,
81
+ "the caller's buffer cannot hold the item",
82
+ ));
83
+ }
84
+ if !items.is_empty() {
85
+ // SAFETY: `holds` shows the buffer is non-null with room for
86
+ // `items`, `new`'s caller made it writable, and caller memory
87
+ // cannot overlap the bridge-owned `items`.
88
+ unsafe { ptr::copy_nonoverlapping(items.as_ptr(), self.ptr, items.len()) }
89
+ }
90
+ Ok(())
91
+ }
92
+ }
93
+
94
+ /// Borrow `len` bytes of caller memory, refusing more than `max` before
95
+ /// reading any. A zero length needs no pointer.
96
+ ///
97
+ /// # Errors
98
+ /// Overflow when `len` exceeds `max`, and invalid input for a null `data`
99
+ /// with a nonzero length.
100
+ ///
101
+ /// # Safety
102
+ /// When `len` is nonzero and at most `max`, and `data` is non-null, `data`
103
+ /// must be valid for reads of `len` bytes that nothing writes until the entry
104
+ /// point returns.
105
+ pub(super) unsafe fn input<'call>(
106
+ data: *const u8,
107
+ len: usize,
108
+ max: usize,
109
+ ) -> Result<&'call [u8], BridgeError> {
110
+ if len > max {
111
+ return Err(BridgeError::overflow(format!(
112
+ "input of {len} bytes exceeds its {max}-byte bound"
113
+ )));
114
+ }
115
+ if len == 0 {
116
+ return Ok(&[]);
117
+ }
118
+ if data.is_null() {
119
+ return Err(BridgeError::invalid("null input with a nonzero length"));
120
+ }
121
+ // SAFETY: the caller made `data` valid for reads of `len` bytes, unchanged
122
+ // for the call.
123
+ Ok(unsafe { slice::from_raw_parts(data, len) })
124
+ }
125
+
126
+ /// Borrow the peer behind a handle.
127
+ ///
128
+ /// # Errors
129
+ /// Invalid input for a null or misaligned handle.
130
+ ///
131
+ /// # Safety
132
+ /// An aligned, non-null `peer` must be a handle from
133
+ /// `reactor_effect_peer_create` that stays undestroyed until the entry point
134
+ /// returns.
135
+ pub(super) unsafe fn peer_ref<'call>(
136
+ peer: *mut ReactorEffectPeer,
137
+ ) -> Result<&'call ReactorEffectPeer, BridgeError> {
138
+ aligned(peer)?;
139
+ // SAFETY: a non-null, aligned `peer` is a live `Box::into_raw` pointer for
140
+ // the call, per the caller.
141
+ unsafe { peer.as_ref() }.ok_or_else(|| BridgeError::invalid("null native peer handle"))
142
+ }
143
+
144
+ /// Refuse a pointer that is not aligned for `T`. A null pointer is aligned.
145
+ fn aligned<T>(ptr: *const T) -> Result<(), BridgeError> {
146
+ if ptr.is_aligned() {
147
+ return Ok(());
148
+ }
149
+ let name = type_name::<T>().rsplit("::").next().unwrap_or_default();
150
+ Err(BridgeError::invalid(format!("misaligned {name} pointer")))
151
+ }
152
+
153
+ #[cfg(test)]
154
+ mod tests {
155
+ use super::*;
156
+
157
+ #[test]
158
+ fn a_slice_view_holds_what_fits_and_null_holds_only_nothing() {
159
+ let mut buffer = [0u8; 4];
160
+ // SAFETY: `buffer` is writable for 4 bytes for the whole test.
161
+ let view = unsafe { OutSlice::new(buffer.as_mut_ptr(), buffer.len()) }.unwrap();
162
+ assert!(view.holds(0) && view.holds(4) && !view.holds(5));
163
+ // SAFETY: a null view is never written.
164
+ let null = unsafe { OutSlice::<u8>::new(ptr::null_mut(), 8) }.unwrap();
165
+ assert!(null.is_null());
166
+ assert!(null.holds(0) && !null.holds(1));
167
+ }
168
+
169
+ #[test]
170
+ fn a_slice_view_copies_to_its_start() {
171
+ let mut buffer = [9u8; 4];
172
+ // SAFETY: `buffer` is writable for 4 bytes for the whole test.
173
+ let mut view = unsafe { OutSlice::new(buffer.as_mut_ptr(), buffer.len()) }.unwrap();
174
+ assert_eq!(view.copy_from(&[1, 2]), Ok(()));
175
+ assert_eq!(view.copy_from(&[]), Ok(()));
176
+ assert_eq!(buffer, [1, 2, 9, 9]);
177
+ }
178
+
179
+ #[test]
180
+ fn a_slice_view_refuses_an_item_it_cannot_hold_and_copies_nothing() {
181
+ let mut buffer = [0u8; 1];
182
+ // SAFETY: `buffer` is writable for 1 byte for the whole test.
183
+ let mut view = unsafe { OutSlice::new(buffer.as_mut_ptr(), buffer.len()) }.unwrap();
184
+ let error = view.copy_from(&[1, 2]).unwrap_err();
185
+ assert_eq!(error.class, FailureClass::Native);
186
+ // SAFETY: a null view is never written.
187
+ let mut null = unsafe { OutSlice::<u8>::new(ptr::null_mut(), 8) }.unwrap();
188
+ assert_eq!(
189
+ null.copy_from(&[1]).unwrap_err().class,
190
+ FailureClass::Native
191
+ );
192
+ assert_eq!(null.copy_from(&[]), Ok(()));
193
+ assert_eq!(buffer, [0]);
194
+ }
195
+
196
+ #[test]
197
+ fn input_needs_a_pointer_only_for_bytes() {
198
+ // SAFETY: a zero length reads nothing.
199
+ let empty = unsafe { input(ptr::null(), 0, 8) };
200
+ assert_eq!(empty, Ok(&[][..]));
201
+ // SAFETY: a null pointer is rejected before any read.
202
+ let null = unsafe { input(ptr::null(), 3, 8) };
203
+ assert_eq!(
204
+ null,
205
+ Err(BridgeError::invalid("null input with a nonzero length"))
206
+ );
207
+ let bytes = [1, 2, 3];
208
+ // SAFETY: `bytes` is readable for its length for the whole test.
209
+ let read = unsafe { input(bytes.as_ptr(), bytes.len(), 3) };
210
+ assert_eq!(read, Ok(&bytes[..]));
211
+ }
212
+
213
+ #[test]
214
+ fn input_refuses_a_length_over_its_bound_before_reading() {
215
+ // SAFETY: nothing is readable behind a dangling pointer, and a length
216
+ // over the bound is refused before any read.
217
+ let over = unsafe { input(NonNull::dangling().as_ptr(), 9, 8) };
218
+ assert_eq!(over.unwrap_err().class, FailureClass::Overflow);
219
+ }
220
+
221
+ #[test]
222
+ fn views_refuse_misaligned_pointers_and_accept_null() {
223
+ let mut words = [0u32; 2];
224
+ #[expect(
225
+ clippy::cast_ptr_alignment,
226
+ reason = "the test needs a misaligned pointer"
227
+ )]
228
+ let misaligned = words
229
+ .as_mut_ptr()
230
+ .cast::<u8>()
231
+ .wrapping_add(1)
232
+ .cast::<u32>();
233
+ // SAFETY: a misaligned pointer is refused before any write.
234
+ let out = unsafe { Out::new(misaligned) };
235
+ assert_eq!(
236
+ out.err(),
237
+ Some(BridgeError::invalid("misaligned u32 pointer"))
238
+ );
239
+ // SAFETY: as above.
240
+ let slice = unsafe { OutSlice::new(misaligned, 1) };
241
+ assert_eq!(
242
+ slice.err().map(|error| error.class),
243
+ Some(FailureClass::InvalidInput)
244
+ );
245
+ // SAFETY: a null view is never written.
246
+ let null = unsafe { Out::<u32>::new(ptr::null_mut()) };
247
+ assert!(matches!(null, Ok(None)));
248
+ // SAFETY: a misaligned handle is refused before it is dereferenced.
249
+ let handle = unsafe { peer_ref(misaligned.cast()) };
250
+ assert_eq!(
251
+ handle.err().map(|error| error.class),
252
+ Some(FailureClass::InvalidInput)
253
+ );
254
+ assert_eq!(words, [0, 0]);
255
+ }
256
+ }