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.
- package/Dockerfile +4 -2
- package/README.md +90 -15
- package/dist/_internal/bridge.d.ts +70 -14
- package/dist/_internal/bridge.d.ts.map +1 -1
- package/dist/_internal/bridge.js +290 -145
- package/dist/_internal/bridge.js.map +1 -1
- package/dist/_internal/isolated/child.d.ts +2 -0
- package/dist/_internal/isolated/child.d.ts.map +1 -0
- package/dist/_internal/isolated/child.js +176 -0
- package/dist/_internal/isolated/child.js.map +1 -0
- package/dist/_internal/isolated/host.d.ts +147 -0
- package/dist/_internal/isolated/host.d.ts.map +1 -0
- package/dist/_internal/isolated/host.js +645 -0
- package/dist/_internal/isolated/host.js.map +1 -0
- package/dist/_internal/isolated/protocol.d.ts +399 -0
- package/dist/_internal/isolated/protocol.d.ts.map +1 -0
- package/dist/_internal/isolated/protocol.js +250 -0
- package/dist/_internal/isolated/protocol.js.map +1 -0
- package/dist/_internal/peer.d.ts +68 -13
- package/dist/_internal/peer.d.ts.map +1 -1
- package/dist/_internal/peer.js +233 -157
- package/dist/_internal/peer.js.map +1 -1
- package/dist/index.d.ts +19 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -33
- package/dist/index.js.map +1 -1
- package/dist/isolated.d.ts +25 -0
- package/dist/isolated.d.ts.map +1 -0
- package/dist/isolated.js +39 -0
- package/dist/isolated.js.map +1 -0
- package/lib/darwin-arm64/libreactor_effect_native.dylib +0 -0
- package/lib/darwin-arm64/native-identity.json +4 -3
- package/lib/linux-x64/libreactor_effect_native.so +0 -0
- package/lib/linux-x64/native-identity.json +4 -3
- package/package.json +7 -3
- package/rust/Cargo.toml +108 -2
- package/rust/build.rs +253 -114
- package/rust/clippy.toml +7 -0
- package/rust/include/reactor_effect_native.h +101 -42
- package/rust/src/abi.rs +258 -0
- package/rust/src/error.rs +149 -0
- package/rust/src/ffi/memory.rs +256 -0
- package/rust/src/ffi/tests.rs +719 -0
- package/rust/src/ffi.rs +474 -0
- package/rust/src/lib.rs +36 -2341
- package/rust/src/peer/callbacks.rs +145 -0
- package/rust/src/peer/media.rs +221 -0
- package/rust/src/peer/owner/tests.rs +188 -0
- package/rust/src/peer/owner.rs +353 -0
- package/rust/src/peer/shared.rs +323 -0
- package/rust/src/peer/tests.rs +513 -0
- package/rust/src/peer.rs +189 -0
- package/rust/src/protocol/event.rs +238 -0
- package/rust/src/protocol/request.rs +347 -0
- package/rust/src/protocol/stats.rs +356 -0
- package/rust/src/protocol.rs +71 -0
- package/rust/src/sync/gate.rs +159 -0
- package/rust/src/sync/notifier.rs +136 -0
- package/rust/src/sync/queue.rs +384 -0
- package/rust/src/sync.rs +20 -0
- package/rust/src/test_support.rs +99 -0
- package/rust-toolchain.toml +7 -0
- package/scripts/stage.mjs +41 -1
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
//! The C ABI, exercised the way a host uses it: through raw pointers and
|
|
2
|
+
//! status codes.
|
|
3
|
+
|
|
4
|
+
use super::*;
|
|
5
|
+
use crate::abi::{Channel, Operation, Ready};
|
|
6
|
+
use crate::peer::{AudioItem, Shared, VideoItem};
|
|
7
|
+
use crate::protocol::Event;
|
|
8
|
+
use crate::sync::Push;
|
|
9
|
+
use crate::test_support::wait_until;
|
|
10
|
+
use serde_json::Value;
|
|
11
|
+
use std::mem::offset_of;
|
|
12
|
+
use std::panic::resume_unwind;
|
|
13
|
+
use std::ptr::NonNull;
|
|
14
|
+
use std::sync::Mutex;
|
|
15
|
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
16
|
+
use std::thread;
|
|
17
|
+
use std::time::Duration;
|
|
18
|
+
|
|
19
|
+
/// A peer handle owned by a test and destroyed on drop, even when an
|
|
20
|
+
/// assertion fails first.
|
|
21
|
+
struct TestPeer(NonNull<ReactorEffectPeer>);
|
|
22
|
+
|
|
23
|
+
// SAFETY: a host may call a live handle's entry points from any thread, which
|
|
24
|
+
// the static assertion that `ReactorEffectPeer: Send + Sync` backs.
|
|
25
|
+
unsafe impl Send for TestPeer {}
|
|
26
|
+
// SAFETY: as for `Send`.
|
|
27
|
+
unsafe impl Sync for TestPeer {}
|
|
28
|
+
|
|
29
|
+
/// What `reactor_effect_peer_call` produced.
|
|
30
|
+
#[derive(Debug)]
|
|
31
|
+
struct Called {
|
|
32
|
+
status: i32,
|
|
33
|
+
response_len: usize,
|
|
34
|
+
response: Vec<u8>,
|
|
35
|
+
failure: String,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl TestPeer {
|
|
39
|
+
fn new() -> Self {
|
|
40
|
+
Self::notifying(None)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn notifying(notify: Option<ReactorEffectNotify>) -> Self {
|
|
44
|
+
Self(NonNull::new(reactor_effect_peer_create(notify)).expect("a peer handle"))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn handle(&self) -> *mut ReactorEffectPeer {
|
|
48
|
+
self.0.as_ptr()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn shared(&self) -> &Shared {
|
|
52
|
+
// SAFETY: the handle stays live until this `TestPeer` drops.
|
|
53
|
+
unsafe { self.0.as_ref() }.shared()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn call(&self, operation: u32, request: &[u8]) -> Called {
|
|
57
|
+
self.call_into(operation, request, CALL_BUFFER_MIN)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn call_into(&self, operation: u32, request: &[u8], response_cap: usize) -> Called {
|
|
61
|
+
let mut response = vec![0; response_cap];
|
|
62
|
+
let mut response_len = usize::MAX;
|
|
63
|
+
let mut failure = ReactorEffectFailure::new("");
|
|
64
|
+
// SAFETY: the handle is live and every buffer is as long as passed.
|
|
65
|
+
let status = unsafe {
|
|
66
|
+
reactor_effect_peer_call(
|
|
67
|
+
self.handle(),
|
|
68
|
+
operation,
|
|
69
|
+
request.as_ptr(),
|
|
70
|
+
request.len(),
|
|
71
|
+
response.as_mut_ptr(),
|
|
72
|
+
response.len(),
|
|
73
|
+
&raw mut response_len,
|
|
74
|
+
&raw mut failure,
|
|
75
|
+
)
|
|
76
|
+
};
|
|
77
|
+
response.truncate(response_len.min(response_cap));
|
|
78
|
+
Called {
|
|
79
|
+
status,
|
|
80
|
+
response_len,
|
|
81
|
+
response,
|
|
82
|
+
failure: text(&failure).to_owned(),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn send(&self, channel: u32, data: &[u8]) -> (i32, String) {
|
|
87
|
+
let mut failure = ReactorEffectFailure::new("");
|
|
88
|
+
// SAFETY: the handle is live and `data` is readable for its length.
|
|
89
|
+
let status = unsafe {
|
|
90
|
+
reactor_effect_peer_send(
|
|
91
|
+
self.handle(),
|
|
92
|
+
channel,
|
|
93
|
+
data.as_ptr(),
|
|
94
|
+
data.len(),
|
|
95
|
+
&raw mut failure,
|
|
96
|
+
)
|
|
97
|
+
};
|
|
98
|
+
(status, text(&failure).to_owned())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Take an event into a buffer of `capacity` bytes: its status, the
|
|
102
|
+
/// length it reported and the bytes it copied.
|
|
103
|
+
fn take_event(&self, capacity: usize) -> (i32, usize, Vec<u8>) {
|
|
104
|
+
let mut out = vec![0; capacity];
|
|
105
|
+
let mut out_len = usize::MAX;
|
|
106
|
+
let out_ptr = if capacity == 0 {
|
|
107
|
+
ptr::null_mut()
|
|
108
|
+
} else {
|
|
109
|
+
out.as_mut_ptr()
|
|
110
|
+
};
|
|
111
|
+
// SAFETY: the handle is live and `out` is writable for `capacity`.
|
|
112
|
+
let status = unsafe {
|
|
113
|
+
reactor_effect_peer_take_event(self.handle(), out_ptr, capacity, &raw mut out_len)
|
|
114
|
+
};
|
|
115
|
+
out.truncate(out_len.min(capacity));
|
|
116
|
+
(status, out_len, out)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn close(&self) {
|
|
120
|
+
// SAFETY: the handle is live.
|
|
121
|
+
unsafe { reactor_effect_peer_close(self.handle()) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn shutdown(&self) -> (i32, String) {
|
|
125
|
+
let mut failure = ReactorEffectFailure::new("");
|
|
126
|
+
// SAFETY: the handle is live and `failure` is writable.
|
|
127
|
+
let status = unsafe { reactor_effect_peer_shutdown(self.handle(), &raw mut failure) };
|
|
128
|
+
(status, text(&failure).to_owned())
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl Drop for TestPeer {
|
|
133
|
+
fn drop(&mut self) {
|
|
134
|
+
// SAFETY: the handle came from `reactor_effect_peer_create`, is
|
|
135
|
+
// destroyed only here, and every call on it has returned.
|
|
136
|
+
unsafe { reactor_effect_peer_destroy(self.handle()) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn text(failure: &ReactorEffectFailure) -> &str {
|
|
141
|
+
std::str::from_utf8(&failure.message[..failure.message_len as usize])
|
|
142
|
+
.expect("failure text is UTF-8")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn video_item(track: u32, fill: u8, metadata: &[u8]) -> VideoItem {
|
|
146
|
+
VideoItem {
|
|
147
|
+
track,
|
|
148
|
+
width: 2,
|
|
149
|
+
height: 1,
|
|
150
|
+
frame_id: u64::MAX,
|
|
151
|
+
timestamp_us: 9_007_199_254_740_993,
|
|
152
|
+
sequence: u64::MAX - 1,
|
|
153
|
+
bgra: vec![fill; 8],
|
|
154
|
+
metadata: metadata.to_vec(),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const OK: i32 = Status::Ok.code();
|
|
159
|
+
const AGAIN: i32 = Status::Again.code();
|
|
160
|
+
const BUFFER_TOO_SMALL: i32 = Status::BufferTooSmall.code();
|
|
161
|
+
const CLOSED: i32 = Status::Closed.code();
|
|
162
|
+
const INVALID_INPUT: i32 = Status::InvalidInput.code();
|
|
163
|
+
const OVERFLOW: i32 = Status::Overflow.code();
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn c_structs_match_the_header_layout() {
|
|
167
|
+
assert_eq!(size_of::<ReactorEffectVideoHeader>(), 48);
|
|
168
|
+
assert_eq!(offset_of!(ReactorEffectVideoHeader, frame_id), 16);
|
|
169
|
+
assert_eq!(offset_of!(ReactorEffectVideoHeader, timestamp_us), 24);
|
|
170
|
+
assert_eq!(offset_of!(ReactorEffectVideoHeader, track), 32);
|
|
171
|
+
assert_eq!(offset_of!(ReactorEffectVideoHeader, sequence), 40);
|
|
172
|
+
assert_eq!(size_of::<ReactorEffectAudioHeader>(), 24);
|
|
173
|
+
assert_eq!(offset_of!(ReactorEffectAudioHeader, track), 12);
|
|
174
|
+
assert_eq!(offset_of!(ReactorEffectAudioHeader, sequence), 16);
|
|
175
|
+
assert_eq!(size_of::<ReactorEffectFailure>(), 1024);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn the_build_identity_is_marked_json_for_this_abi() {
|
|
180
|
+
// SAFETY: the export returns a pointer to a static C string.
|
|
181
|
+
let exported = unsafe { CStr::from_ptr(reactor_effect_build_identity()) };
|
|
182
|
+
assert_eq!(exported, BUILD_IDENTITY);
|
|
183
|
+
assert_eq!(reactor_effect_abi_version(), ABI_VERSION);
|
|
184
|
+
|
|
185
|
+
let identity = exported.to_str().expect("UTF-8");
|
|
186
|
+
let json = identity
|
|
187
|
+
.strip_prefix("reactor-effect-native:build-identity:")
|
|
188
|
+
.and_then(|rest| rest.strip_suffix(":end"))
|
|
189
|
+
.expect("the identity is inside its marker");
|
|
190
|
+
let build: Value = serde_json::from_str(json).expect("the identity is JSON");
|
|
191
|
+
assert_eq!(build["schemaVersion"], 1);
|
|
192
|
+
assert_eq!(
|
|
193
|
+
build["abiVersion"], ABI_VERSION,
|
|
194
|
+
"build.rs must declare this ABI"
|
|
195
|
+
);
|
|
196
|
+
let source = build["sourceSha256"].as_str().unwrap();
|
|
197
|
+
assert!(source.len() == 64 && source.bytes().all(|byte| byte.is_ascii_hexdigit()));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[test]
|
|
201
|
+
fn take_event_reports_the_size_then_copies_the_event_once() {
|
|
202
|
+
let peer = TestPeer::new();
|
|
203
|
+
let packet = Event::Message {
|
|
204
|
+
channel: Channel::Control,
|
|
205
|
+
bytes: b"abc",
|
|
206
|
+
}
|
|
207
|
+
.to_packet()
|
|
208
|
+
.unwrap();
|
|
209
|
+
assert_eq!(
|
|
210
|
+
peer.shared().events.try_push(packet.clone()),
|
|
211
|
+
Push::Accepted
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
assert_eq!(peer.take_event(0), (BUFFER_TOO_SMALL, packet.len(), vec![]));
|
|
215
|
+
assert_eq!(
|
|
216
|
+
peer.shared().events.counts().queued,
|
|
217
|
+
1,
|
|
218
|
+
"a short take keeps the event"
|
|
219
|
+
);
|
|
220
|
+
assert_eq!(
|
|
221
|
+
peer.take_event(packet.len()),
|
|
222
|
+
(OK, packet.len(), packet.clone())
|
|
223
|
+
);
|
|
224
|
+
assert_eq!(peer.take_event(packet.len()), (AGAIN, 0, vec![]));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn media_takes_copy_typed_frames_into_caller_buffers() {
|
|
229
|
+
let peer = TestPeer::new();
|
|
230
|
+
peer.shared().push_video(video_item(3, 0x21, b"meta"));
|
|
231
|
+
let pcm = vec![1i16, -2, 300, -400];
|
|
232
|
+
peer.shared().push_audio(AudioItem {
|
|
233
|
+
track: 1,
|
|
234
|
+
sample_rate: 48_000,
|
|
235
|
+
channels: 2,
|
|
236
|
+
sequence: 9,
|
|
237
|
+
pcm: pcm.clone(),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
let mut header = ReactorEffectVideoHeader::default();
|
|
241
|
+
let mut bgra = vec![0u8; 8];
|
|
242
|
+
let mut metadata = vec![0u8; 3];
|
|
243
|
+
let mut take_video = |bgra: &mut [u8], metadata: &mut [u8]| {
|
|
244
|
+
// SAFETY: the handle is live and each buffer is writable for its length.
|
|
245
|
+
unsafe {
|
|
246
|
+
reactor_effect_peer_take_video(
|
|
247
|
+
peer.handle(),
|
|
248
|
+
&raw mut header,
|
|
249
|
+
bgra.as_mut_ptr(),
|
|
250
|
+
bgra.len(),
|
|
251
|
+
metadata.as_mut_ptr(),
|
|
252
|
+
metadata.len(),
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
assert_eq!(take_video(&mut bgra, &mut metadata), BUFFER_TOO_SMALL);
|
|
257
|
+
assert_eq!(
|
|
258
|
+
peer.shared().video.counts().queued,
|
|
259
|
+
1,
|
|
260
|
+
"a short take keeps the frame"
|
|
261
|
+
);
|
|
262
|
+
metadata.resize(4, 0);
|
|
263
|
+
assert_eq!(take_video(&mut bgra, &mut metadata), OK);
|
|
264
|
+
assert_eq!(take_video(&mut bgra, &mut metadata), AGAIN);
|
|
265
|
+
assert_eq!(header, video_item(3, 0x21, b"meta").header());
|
|
266
|
+
assert_eq!(bgra, [0x21; 8]);
|
|
267
|
+
assert_eq!(metadata, b"meta");
|
|
268
|
+
|
|
269
|
+
let mut audio = ReactorEffectAudioHeader::default();
|
|
270
|
+
let mut samples = vec![0i16; 3];
|
|
271
|
+
let mut take_audio = |samples: &mut [i16]| {
|
|
272
|
+
// SAFETY: the handle is live and `samples` is writable for its length.
|
|
273
|
+
unsafe {
|
|
274
|
+
reactor_effect_peer_take_audio(
|
|
275
|
+
peer.handle(),
|
|
276
|
+
&raw mut audio,
|
|
277
|
+
samples.as_mut_ptr(),
|
|
278
|
+
samples.len(),
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
assert_eq!(take_audio(&mut samples), BUFFER_TOO_SMALL);
|
|
283
|
+
assert_eq!(
|
|
284
|
+
peer.shared().audio.counts().queued,
|
|
285
|
+
1,
|
|
286
|
+
"a short take keeps the block"
|
|
287
|
+
);
|
|
288
|
+
samples.resize(4, 0);
|
|
289
|
+
assert_eq!(take_audio(&mut samples), OK);
|
|
290
|
+
assert_eq!(take_audio(&mut samples), AGAIN);
|
|
291
|
+
assert_eq!(
|
|
292
|
+
audio,
|
|
293
|
+
ReactorEffectAudioHeader {
|
|
294
|
+
sample_rate: 48_000,
|
|
295
|
+
channels: 2,
|
|
296
|
+
samples: 4,
|
|
297
|
+
track: 1,
|
|
298
|
+
sequence: 9,
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
assert_eq!(samples, pcm);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[test]
|
|
305
|
+
fn a_null_media_buffer_asks_for_the_header_alone() {
|
|
306
|
+
let peer = TestPeer::new();
|
|
307
|
+
peer.shared().push_video(video_item(0, 1, b""));
|
|
308
|
+
let mut header = ReactorEffectVideoHeader::default();
|
|
309
|
+
// SAFETY: the handle is live, `header` is writable and the buffers are null.
|
|
310
|
+
let status = unsafe {
|
|
311
|
+
reactor_effect_peer_take_video(
|
|
312
|
+
peer.handle(),
|
|
313
|
+
&raw mut header,
|
|
314
|
+
ptr::null_mut(),
|
|
315
|
+
0,
|
|
316
|
+
ptr::null_mut(),
|
|
317
|
+
0,
|
|
318
|
+
)
|
|
319
|
+
};
|
|
320
|
+
assert_eq!(status, BUFFER_TOO_SMALL);
|
|
321
|
+
assert_eq!((header.data_len, header.metadata_len), (8, 0));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[test]
|
|
325
|
+
fn closing_fences_every_take_and_host_call() {
|
|
326
|
+
let peer = TestPeer::new();
|
|
327
|
+
peer.shared().push_video(video_item(0, 1, b""));
|
|
328
|
+
peer.close();
|
|
329
|
+
|
|
330
|
+
let snapshot = peer.call(Operation::MediaSnapshot as u32, b"");
|
|
331
|
+
assert_eq!(
|
|
332
|
+
(snapshot.status, snapshot.failure.as_str()),
|
|
333
|
+
(CLOSED, "native peer is closed")
|
|
334
|
+
);
|
|
335
|
+
assert_eq!(peer.send(Channel::Data as u32, b"late").0, CLOSED);
|
|
336
|
+
assert_eq!(peer.take_event(0), (CLOSED, 0, vec![]));
|
|
337
|
+
|
|
338
|
+
let mut header = ReactorEffectVideoHeader::default();
|
|
339
|
+
let mut bgra = [0u8; 8];
|
|
340
|
+
// SAFETY: the handle is live and each buffer is writable for its length.
|
|
341
|
+
let video = unsafe {
|
|
342
|
+
reactor_effect_peer_take_video(
|
|
343
|
+
peer.handle(),
|
|
344
|
+
&raw mut header,
|
|
345
|
+
bgra.as_mut_ptr(),
|
|
346
|
+
bgra.len(),
|
|
347
|
+
ptr::null_mut(),
|
|
348
|
+
0,
|
|
349
|
+
)
|
|
350
|
+
};
|
|
351
|
+
assert_eq!(video, CLOSED, "closing discards queued media");
|
|
352
|
+
let mut audio_header = ReactorEffectAudioHeader::default();
|
|
353
|
+
let mut pcm = [0i16; 4];
|
|
354
|
+
// SAFETY: the handle is live and each buffer is writable for its length.
|
|
355
|
+
let audio = unsafe {
|
|
356
|
+
reactor_effect_peer_take_audio(
|
|
357
|
+
peer.handle(),
|
|
358
|
+
&raw mut audio_header,
|
|
359
|
+
pcm.as_mut_ptr(),
|
|
360
|
+
pcm.len(),
|
|
361
|
+
)
|
|
362
|
+
};
|
|
363
|
+
assert_eq!(audio, CLOSED);
|
|
364
|
+
assert_eq!(peer.shutdown(), (OK, String::new()));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[test]
|
|
368
|
+
fn readiness_reaches_the_host_on_the_notifier_thread() {
|
|
369
|
+
static READY: AtomicU32 = AtomicU32::new(0);
|
|
370
|
+
static THREADS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
|
371
|
+
extern "C" fn record(ready: u32) {
|
|
372
|
+
let name = thread::current().name().unwrap_or_default().to_owned();
|
|
373
|
+
THREADS.lock().unwrap().push(name);
|
|
374
|
+
READY.fetch_or(ready, Ordering::AcqRel);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let peer = TestPeer::notifying(Some(record));
|
|
378
|
+
peer.shared().emit(&Event::Ice { candidate: None });
|
|
379
|
+
wait_until("event readiness", Duration::from_secs(5), || {
|
|
380
|
+
READY.load(Ordering::Acquire) & Ready::Events.bit() != 0
|
|
381
|
+
});
|
|
382
|
+
peer.shared().push_video(video_item(0, 1, b""));
|
|
383
|
+
wait_until("video readiness", Duration::from_secs(5), || {
|
|
384
|
+
READY.load(Ordering::Acquire) & Ready::Video.bit() != 0
|
|
385
|
+
});
|
|
386
|
+
assert_eq!(peer.shutdown(), (OK, String::new()));
|
|
387
|
+
assert_eq!(
|
|
388
|
+
peer.shutdown(),
|
|
389
|
+
(OK, String::new()),
|
|
390
|
+
"shutdown is idempotent"
|
|
391
|
+
);
|
|
392
|
+
let threads = THREADS.lock().unwrap();
|
|
393
|
+
assert!(
|
|
394
|
+
threads.iter().all(|name| name == "reactor-effect-notify"),
|
|
395
|
+
"only the notifier thread calls the host: {threads:?}"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn calls_report_their_failure_class_and_diagnostic() {
|
|
401
|
+
let peer = TestPeer::new();
|
|
402
|
+
|
|
403
|
+
let (status, failure) = peer.send(Channel::Data as u32, b"early");
|
|
404
|
+
assert_eq!(
|
|
405
|
+
(status, failure.as_str()),
|
|
406
|
+
(Status::ChannelClosed.code(), "data channel is not open")
|
|
407
|
+
);
|
|
408
|
+
// SAFETY: the handle is live; a null failure pointer is allowed.
|
|
409
|
+
let unknown_channel =
|
|
410
|
+
unsafe { reactor_effect_peer_send(peer.handle(), 7, ptr::null(), 0, ptr::null_mut()) };
|
|
411
|
+
assert_eq!(unknown_channel, INVALID_INPUT);
|
|
412
|
+
|
|
413
|
+
let unknown = peer.call(99, b"");
|
|
414
|
+
assert_eq!(
|
|
415
|
+
(
|
|
416
|
+
unknown.status,
|
|
417
|
+
unknown.response_len,
|
|
418
|
+
unknown.failure.as_str()
|
|
419
|
+
),
|
|
420
|
+
(INVALID_INPUT, 0, "unknown native call operation")
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
let oversized = peer.call(
|
|
424
|
+
Operation::Prepare as u32,
|
|
425
|
+
&vec![b' '; MAX_REQUEST_BYTES + 1],
|
|
426
|
+
);
|
|
427
|
+
assert_eq!(oversized.status, Status::Overflow.code());
|
|
428
|
+
|
|
429
|
+
let short = peer.call_into(Operation::MediaSnapshot as u32, b"", 16);
|
|
430
|
+
assert_eq!(
|
|
431
|
+
(short.status, short.response_len),
|
|
432
|
+
(BUFFER_TOO_SMALL, CALL_BUFFER_MIN)
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
let snapshot = peer.call(Operation::MediaSnapshot as u32, b"");
|
|
436
|
+
assert_eq!(snapshot.status, OK);
|
|
437
|
+
assert_eq!(snapshot.response_len, snapshot.response.len());
|
|
438
|
+
let snapshot: Value = serde_json::from_slice(&snapshot.response).expect("snapshot JSON");
|
|
439
|
+
assert_eq!(snapshot["closed"], false);
|
|
440
|
+
assert_eq!(snapshot["droppedVideo"], "0");
|
|
441
|
+
|
|
442
|
+
let bad_answer = peer.call(Operation::Answer as u32, b"");
|
|
443
|
+
assert_eq!(
|
|
444
|
+
(bad_answer.status, bad_answer.failure.as_str()),
|
|
445
|
+
(INVALID_INPUT, "answer SDP is empty")
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
fn a_null_handle_or_required_pointer_is_invalid_input() {
|
|
451
|
+
let null = ptr::null_mut();
|
|
452
|
+
let mut response = vec![0u8; CALL_BUFFER_MIN];
|
|
453
|
+
let mut len = 0usize;
|
|
454
|
+
let mut failure = ReactorEffectFailure::new("");
|
|
455
|
+
let mut video = ReactorEffectVideoHeader::default();
|
|
456
|
+
let mut audio = ReactorEffectAudioHeader::default();
|
|
457
|
+
|
|
458
|
+
// SAFETY: a null handle is rejected before any other argument is used.
|
|
459
|
+
let call = unsafe {
|
|
460
|
+
reactor_effect_peer_call(
|
|
461
|
+
null,
|
|
462
|
+
Operation::MediaSnapshot as u32,
|
|
463
|
+
ptr::null(),
|
|
464
|
+
0,
|
|
465
|
+
response.as_mut_ptr(),
|
|
466
|
+
response.len(),
|
|
467
|
+
&raw mut len,
|
|
468
|
+
&raw mut failure,
|
|
469
|
+
)
|
|
470
|
+
};
|
|
471
|
+
assert_eq!(
|
|
472
|
+
(call, text(&failure)),
|
|
473
|
+
(INVALID_INPUT, "null native peer handle")
|
|
474
|
+
);
|
|
475
|
+
// SAFETY: as above.
|
|
476
|
+
let send = unsafe { reactor_effect_peer_send(null, 0, ptr::null(), 0, ptr::null_mut()) };
|
|
477
|
+
// SAFETY: as above.
|
|
478
|
+
let event = unsafe { reactor_effect_peer_take_event(null, ptr::null_mut(), 0, &raw mut len) };
|
|
479
|
+
// SAFETY: as above.
|
|
480
|
+
let take_video = unsafe {
|
|
481
|
+
reactor_effect_peer_take_video(null, &raw mut video, ptr::null_mut(), 0, ptr::null_mut(), 0)
|
|
482
|
+
};
|
|
483
|
+
// SAFETY: as above.
|
|
484
|
+
let take_audio =
|
|
485
|
+
unsafe { reactor_effect_peer_take_audio(null, &raw mut audio, ptr::null_mut(), 0) };
|
|
486
|
+
// SAFETY: as above.
|
|
487
|
+
let shutdown = unsafe { reactor_effect_peer_shutdown(null, ptr::null_mut()) };
|
|
488
|
+
assert_eq!(
|
|
489
|
+
[send, event, take_video, take_audio, shutdown],
|
|
490
|
+
[INVALID_INPUT; 5]
|
|
491
|
+
);
|
|
492
|
+
// SAFETY: closing or destroying a null handle does nothing.
|
|
493
|
+
unsafe { reactor_effect_peer_close(null) };
|
|
494
|
+
// SAFETY: as above.
|
|
495
|
+
unsafe { reactor_effect_peer_destroy(null) };
|
|
496
|
+
|
|
497
|
+
let peer = TestPeer::new();
|
|
498
|
+
// SAFETY: the handle is live and the buffers are writable; the null
|
|
499
|
+
// response length is rejected.
|
|
500
|
+
let no_length = unsafe {
|
|
501
|
+
reactor_effect_peer_call(
|
|
502
|
+
peer.handle(),
|
|
503
|
+
Operation::MediaSnapshot as u32,
|
|
504
|
+
ptr::null(),
|
|
505
|
+
0,
|
|
506
|
+
response.as_mut_ptr(),
|
|
507
|
+
response.len(),
|
|
508
|
+
ptr::null_mut(),
|
|
509
|
+
&raw mut failure,
|
|
510
|
+
)
|
|
511
|
+
};
|
|
512
|
+
assert_eq!(
|
|
513
|
+
(no_length, text(&failure)),
|
|
514
|
+
(INVALID_INPUT, "call requires a response buffer")
|
|
515
|
+
);
|
|
516
|
+
// SAFETY: the handle is live; the null request with a length is rejected.
|
|
517
|
+
let no_request = unsafe {
|
|
518
|
+
reactor_effect_peer_call(
|
|
519
|
+
peer.handle(),
|
|
520
|
+
Operation::Prepare as u32,
|
|
521
|
+
ptr::null(),
|
|
522
|
+
8,
|
|
523
|
+
response.as_mut_ptr(),
|
|
524
|
+
response.len(),
|
|
525
|
+
&raw mut len,
|
|
526
|
+
&raw mut failure,
|
|
527
|
+
)
|
|
528
|
+
};
|
|
529
|
+
assert_eq!(
|
|
530
|
+
(no_request, text(&failure)),
|
|
531
|
+
(INVALID_INPUT, "null input with a nonzero length")
|
|
532
|
+
);
|
|
533
|
+
// SAFETY: the handle is live; the null data with a length is rejected.
|
|
534
|
+
let no_data =
|
|
535
|
+
unsafe { reactor_effect_peer_send(peer.handle(), 0, ptr::null(), 4, ptr::null_mut()) };
|
|
536
|
+
// SAFETY: the handle is live; the null length pointer is rejected.
|
|
537
|
+
let no_out_len = unsafe {
|
|
538
|
+
reactor_effect_peer_take_event(peer.handle(), ptr::null_mut(), 0, ptr::null_mut())
|
|
539
|
+
};
|
|
540
|
+
// SAFETY: the handle is live; a null buffer with a capacity is rejected.
|
|
541
|
+
let no_out =
|
|
542
|
+
unsafe { reactor_effect_peer_take_event(peer.handle(), ptr::null_mut(), 8, &raw mut len) };
|
|
543
|
+
// SAFETY: the handle is live; the null header is rejected.
|
|
544
|
+
let no_video_header = unsafe {
|
|
545
|
+
reactor_effect_peer_take_video(
|
|
546
|
+
peer.handle(),
|
|
547
|
+
ptr::null_mut(),
|
|
548
|
+
ptr::null_mut(),
|
|
549
|
+
0,
|
|
550
|
+
ptr::null_mut(),
|
|
551
|
+
0,
|
|
552
|
+
)
|
|
553
|
+
};
|
|
554
|
+
// SAFETY: the handle is live; the null header is rejected.
|
|
555
|
+
let no_audio_header = unsafe {
|
|
556
|
+
reactor_effect_peer_take_audio(peer.handle(), ptr::null_mut(), ptr::null_mut(), 0)
|
|
557
|
+
};
|
|
558
|
+
assert_eq!(
|
|
559
|
+
[
|
|
560
|
+
no_data,
|
|
561
|
+
no_out_len,
|
|
562
|
+
no_out,
|
|
563
|
+
no_video_header,
|
|
564
|
+
no_audio_header
|
|
565
|
+
],
|
|
566
|
+
[INVALID_INPUT; 5]
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
#[test]
|
|
571
|
+
fn a_length_over_its_bound_is_refused_before_caller_memory_is_read() {
|
|
572
|
+
let peer = TestPeer::new();
|
|
573
|
+
// Nothing is readable at a dangling address, so only the bound checks
|
|
574
|
+
// keep these calls from reading it.
|
|
575
|
+
let nowhere = NonNull::<u8>::dangling().as_ptr();
|
|
576
|
+
let mut response = vec![0u8; CALL_BUFFER_MIN];
|
|
577
|
+
let mut len = 0usize;
|
|
578
|
+
let mut failure = ReactorEffectFailure::new("");
|
|
579
|
+
|
|
580
|
+
// SAFETY: the handle is live and the buffers are writable; a request
|
|
581
|
+
// length over its bound is refused before `request` is read.
|
|
582
|
+
let call = unsafe {
|
|
583
|
+
reactor_effect_peer_call(
|
|
584
|
+
peer.handle(),
|
|
585
|
+
Operation::Prepare as u32,
|
|
586
|
+
nowhere,
|
|
587
|
+
MAX_REQUEST_BYTES + 1,
|
|
588
|
+
response.as_mut_ptr(),
|
|
589
|
+
response.len(),
|
|
590
|
+
&raw mut len,
|
|
591
|
+
&raw mut failure,
|
|
592
|
+
)
|
|
593
|
+
};
|
|
594
|
+
assert_eq!(call, OVERFLOW);
|
|
595
|
+
// SAFETY: the handle is live; a message length over its bound is refused
|
|
596
|
+
// before `data` is read.
|
|
597
|
+
let send = unsafe {
|
|
598
|
+
reactor_effect_peer_send(
|
|
599
|
+
peer.handle(),
|
|
600
|
+
Channel::Data as u32,
|
|
601
|
+
nowhere,
|
|
602
|
+
MAX_MESSAGE_BYTES + 1,
|
|
603
|
+
&raw mut failure,
|
|
604
|
+
)
|
|
605
|
+
};
|
|
606
|
+
assert_eq!(
|
|
607
|
+
(send, text(&failure)),
|
|
608
|
+
(
|
|
609
|
+
OVERFLOW,
|
|
610
|
+
"input of 262145 bytes exceeds its 262144-byte bound"
|
|
611
|
+
)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/// A pointer one byte into `words`, misaligned for any type wider than a byte.
|
|
616
|
+
fn misaligned<T>(words: &mut [u64]) -> *mut T {
|
|
617
|
+
words.as_mut_ptr().cast::<u8>().wrapping_add(1).cast()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#[test]
|
|
621
|
+
fn a_misaligned_pointer_is_invalid_input_and_nothing_is_written() {
|
|
622
|
+
let peer = TestPeer::new();
|
|
623
|
+
let mut words = [0u64; 8];
|
|
624
|
+
let mut response = vec![0u8; CALL_BUFFER_MIN];
|
|
625
|
+
let mut len = usize::MAX;
|
|
626
|
+
let mut audio_header = ReactorEffectAudioHeader::default();
|
|
627
|
+
|
|
628
|
+
// SAFETY: the handle is live and the buffers are writable; the misaligned
|
|
629
|
+
// failure pointer fails the call before its body runs.
|
|
630
|
+
let call = unsafe {
|
|
631
|
+
reactor_effect_peer_call(
|
|
632
|
+
peer.handle(),
|
|
633
|
+
Operation::MediaSnapshot as u32,
|
|
634
|
+
ptr::null(),
|
|
635
|
+
0,
|
|
636
|
+
response.as_mut_ptr(),
|
|
637
|
+
response.len(),
|
|
638
|
+
&raw mut len,
|
|
639
|
+
misaligned(&mut words),
|
|
640
|
+
)
|
|
641
|
+
};
|
|
642
|
+
assert_eq!((call, len), (INVALID_INPUT, usize::MAX), "the call ran");
|
|
643
|
+
// SAFETY: the handle is live; the misaligned response length is refused.
|
|
644
|
+
let response_len = unsafe {
|
|
645
|
+
reactor_effect_peer_call(
|
|
646
|
+
peer.handle(),
|
|
647
|
+
Operation::MediaSnapshot as u32,
|
|
648
|
+
ptr::null(),
|
|
649
|
+
0,
|
|
650
|
+
response.as_mut_ptr(),
|
|
651
|
+
response.len(),
|
|
652
|
+
misaligned(&mut words),
|
|
653
|
+
ptr::null_mut(),
|
|
654
|
+
)
|
|
655
|
+
};
|
|
656
|
+
// SAFETY: the handle is live; the misaligned length pointer is refused.
|
|
657
|
+
let event = unsafe {
|
|
658
|
+
reactor_effect_peer_take_event(peer.handle(), ptr::null_mut(), 0, misaligned(&mut words))
|
|
659
|
+
};
|
|
660
|
+
// SAFETY: the handle is live; the misaligned header is refused.
|
|
661
|
+
let video = unsafe {
|
|
662
|
+
reactor_effect_peer_take_video(
|
|
663
|
+
peer.handle(),
|
|
664
|
+
misaligned(&mut words),
|
|
665
|
+
ptr::null_mut(),
|
|
666
|
+
0,
|
|
667
|
+
ptr::null_mut(),
|
|
668
|
+
0,
|
|
669
|
+
)
|
|
670
|
+
};
|
|
671
|
+
// SAFETY: the handle is live; the misaligned sample buffer is refused.
|
|
672
|
+
let audio = unsafe {
|
|
673
|
+
reactor_effect_peer_take_audio(
|
|
674
|
+
peer.handle(),
|
|
675
|
+
&raw mut audio_header,
|
|
676
|
+
misaligned(&mut words),
|
|
677
|
+
4,
|
|
678
|
+
)
|
|
679
|
+
};
|
|
680
|
+
// SAFETY: a misaligned handle is refused before it is dereferenced.
|
|
681
|
+
let send = unsafe {
|
|
682
|
+
reactor_effect_peer_send(misaligned(&mut words), 0, ptr::null(), 0, ptr::null_mut())
|
|
683
|
+
};
|
|
684
|
+
// SAFETY: as above.
|
|
685
|
+
let shutdown = unsafe { reactor_effect_peer_shutdown(misaligned(&mut words), ptr::null_mut()) };
|
|
686
|
+
assert_eq!(
|
|
687
|
+
[response_len, event, video, audio, send, shutdown],
|
|
688
|
+
[INVALID_INPUT; 6]
|
|
689
|
+
);
|
|
690
|
+
// SAFETY: closing or destroying a misaligned handle does nothing.
|
|
691
|
+
unsafe { reactor_effect_peer_close(misaligned(&mut words)) };
|
|
692
|
+
// SAFETY: as above.
|
|
693
|
+
unsafe { reactor_effect_peer_destroy(misaligned(&mut words)) };
|
|
694
|
+
assert_eq!(words, [0; 8], "a misaligned pointer was written");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#[test]
|
|
698
|
+
fn a_panic_is_a_native_failure_not_an_unwind_into_c() {
|
|
699
|
+
let mut failure = ReactorEffectFailure::new("");
|
|
700
|
+
// SAFETY: `failure` is writable for the call.
|
|
701
|
+
let out = unsafe { Out::new(&raw mut failure) };
|
|
702
|
+
// `resume_unwind` panics without running the panic hook's report.
|
|
703
|
+
let status = status_of(out, || resume_unwind(Box::new("boom")));
|
|
704
|
+
assert_eq!(status, Status::Native.code());
|
|
705
|
+
assert_eq!(text(&failure), "native bridge panicked");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
#[test]
|
|
709
|
+
fn failure_text_is_truncated_on_a_character_boundary() {
|
|
710
|
+
let long = "é".repeat(FAILURE_MESSAGE_BYTES);
|
|
711
|
+
let failure = ReactorEffectFailure::new(&long);
|
|
712
|
+
assert_eq!(failure.message_len as usize, FAILURE_MESSAGE_BYTES);
|
|
713
|
+
assert_eq!(text(&failure), "é".repeat(FAILURE_MESSAGE_BYTES / 2));
|
|
714
|
+
|
|
715
|
+
assert_eq!(truncate("aéb", 2), "a", "a split character is dropped");
|
|
716
|
+
assert_eq!(truncate("aéb", 3), "aé");
|
|
717
|
+
assert_eq!(truncate("short", 64), "short");
|
|
718
|
+
assert_eq!(truncate("", 0), "");
|
|
719
|
+
}
|