quietquic 0.1.0.alpha.3
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.
- checksums.yaml +7 -0
- data/HISTORY.md +79 -0
- data/LICENSE +13 -0
- data/README.md +318 -0
- data/examples/cooperative_async.rb +91 -0
- data/examples/echo_client.rb +40 -0
- data/examples/echo_server.rb +48 -0
- data/ext/quietquic/Cargo.lock +1377 -0
- data/ext/quietquic/Cargo.toml +25 -0
- data/ext/quietquic/extconf.rb +7 -0
- data/ext/quietquic/src/client.rs +89 -0
- data/ext/quietquic/src/config.rs +292 -0
- data/ext/quietquic/src/conn.rs +283 -0
- data/ext/quietquic/src/errors.rs +140 -0
- data/ext/quietquic/src/lib.rs +59 -0
- data/ext/quietquic/src/pending.rs +386 -0
- data/ext/quietquic/src/runtime.rs +72 -0
- data/ext/quietquic/src/server.rs +259 -0
- data/lib/quietquic/await.rb +22 -0
- data/lib/quietquic/client.rb +40 -0
- data/lib/quietquic/connection.rb +55 -0
- data/lib/quietquic/server.rb +70 -0
- data/lib/quietquic/stream.rb +79 -0
- data/lib/quietquic/version.rb +4 -0
- data/lib/quietquic.rb +8 -0
- metadata +125 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
//! `QuietQUIC::Native::Connection` and `QuietQUIC::Native::Stream` — the FFI
|
|
3
|
+
//! wrappers over the crate's [`quietquic_core::conn::Connection`] and
|
|
4
|
+
//! [`quietquic_core::conn::Stream`]. A `NativeConnection` is produced once by
|
|
5
|
+
//! both `Server#accept` and `Client#connect` (the single post-handshake type);
|
|
6
|
+
//! Task 7 grows it into the stream-carrying handle and adds `NativeStream`.
|
|
7
|
+
//!
|
|
8
|
+
//! ## Sharing model — WHY THERE IS NO LOCK AROUND THE CONNECTION
|
|
9
|
+
//!
|
|
10
|
+
//! Every crate `Connection` method (`open_stream`, `accept_stream`, `close`,
|
|
11
|
+
//! `remote_address`) takes `&self`: the `Connection` is a lightweight handle
|
|
12
|
+
//! that talks to the driver over a cloneable `tokio::sync::mpsc` command
|
|
13
|
+
//! channel (see the crate's `conn.rs`). It is `Send` but not `Clone`, so we
|
|
14
|
+
//! park it behind an `Arc` and share `&*conn` into each op's future. Because the
|
|
15
|
+
//! methods are `&self`, concurrent `open_stream`/`accept_stream`/`close` all
|
|
16
|
+
//! share the one handle with NO mutex — there is no `&mut` to guard and thus no
|
|
17
|
+
//! deadlock surface.
|
|
18
|
+
//!
|
|
19
|
+
//! ### No latching-shutdown pattern needed here (unlike `NativeServer`)
|
|
20
|
+
//!
|
|
21
|
+
//! `NativeServer::accept` had to hold an async mutex across `server.accept()`
|
|
22
|
+
//! because the crate `Server::accept` takes `&mut self`; a parked accept then
|
|
23
|
+
//! blocked a `close` that needed the same mutex, forcing the
|
|
24
|
+
//! `closed: AtomicBool` + `Notify` + `select!` latch. Here NOTHING is the case:
|
|
25
|
+
//! - `Connection::accept_stream(&self)` parks in the crate's own driver channel,
|
|
26
|
+
//! holding no lock on our side.
|
|
27
|
+
//! - `Connection::close(&self)` is a plain `&self` command-send; it needs no
|
|
28
|
+
//! lock and races nothing. When the driver tears the connection down it fires
|
|
29
|
+
//! every parked accept/read reply with `ConnError::Closed` (see the crate's
|
|
30
|
+
//! `ConnState::fail_all`), so a parked `accept_stream`/`read_to_end` wakes on
|
|
31
|
+
//! its own — we do not have to unpark it.
|
|
32
|
+
//!
|
|
33
|
+
//! So `close` can always proceed for 0/1/N parked stream ops without any latch.
|
|
34
|
+
//!
|
|
35
|
+
//! ### `NativeStream` and the one `&mut self` seam
|
|
36
|
+
//!
|
|
37
|
+
//! The crate `Stream` methods (`write_all`, `finish`, `read_to_end`) take
|
|
38
|
+
//! `&mut self`. A `Stream` is again just a `StreamId` + cloneable `CmdSender`,
|
|
39
|
+
//! but the `&mut` means we need interior mutability to call them from a shared
|
|
40
|
+
//! Ruby object. We wrap it in `Arc<tokio::sync::Mutex<CoreStream>>` and lock it
|
|
41
|
+
//! inside each op's future — an async mutex, so a parked `read_to_end` yields
|
|
42
|
+
//! the worker rather than blocking it, and no `std` lock is ever held across an
|
|
43
|
+
//! await. This lock only serializes ops on the SAME stream (the natural
|
|
44
|
+
//! contract: you don't concurrently read and finish one stream); it never
|
|
45
|
+
//! interacts with the connection's `close`, so the Task 5 deadlock cannot recur.
|
|
46
|
+
//!
|
|
47
|
+
//! FFI thread-safety: every op yields plain Rust data (`()` / `Vec<u8>` / a
|
|
48
|
+
//! `CoreStream` handle); the Ruby `NativeStream` object and the binary `String`
|
|
49
|
+
//! are built by `to_ruby` on the Ruby thread. No magnus `Value` is ever
|
|
50
|
+
//! constructed on a tokio worker thread. Read bytes are returned as a Ruby
|
|
51
|
+
//! `String` forced to `Encoding::BINARY` via `Ruby::str_from_slice`.
|
|
52
|
+
|
|
53
|
+
use std::net::SocketAddr;
|
|
54
|
+
use std::sync::Arc;
|
|
55
|
+
|
|
56
|
+
use magnus::{method, prelude::*, Error, IntoValue, RModule, RString, Ruby};
|
|
57
|
+
use tokio::sync::Mutex as AsyncMutex;
|
|
58
|
+
|
|
59
|
+
use quietquic_core::conn::{
|
|
60
|
+
Connection as CoreConnection, ConnError, RecvStream as CoreRecvStream,
|
|
61
|
+
SendStream as CoreSendStream,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
use crate::errors::{ErrorKind, MappedError};
|
|
65
|
+
use crate::pending::PendingOp;
|
|
66
|
+
|
|
67
|
+
/// Map a crate [`ConnError`] to the classified [`MappedError`] the bridge
|
|
68
|
+
/// raises. A stream-level failure (reset/stopped/refused) becomes
|
|
69
|
+
/// `QuietQUIC::StreamError`; the driver being gone (connection closed or lost
|
|
70
|
+
/// after establishment) becomes `QuietQUIC::ConnectionLost`.
|
|
71
|
+
fn map_conn_error(e: ConnError) -> MappedError {
|
|
72
|
+
match e {
|
|
73
|
+
ConnError::Closed => MappedError::new(ErrorKind::ConnectionLost, "connection closed"),
|
|
74
|
+
other => MappedError::new(ErrorKind::Stream, format!("stream error: {other}")),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// `QuietQUIC::Native::Connection` — TypedData wrapper over the crate's
|
|
79
|
+
/// post-handshake [`CoreConnection`].
|
|
80
|
+
///
|
|
81
|
+
/// `Send` because the crate `Connection` is `Send` (its command channel +
|
|
82
|
+
/// handle + addr are all `Send`) and `SocketAddr` is `Copy + Send`. Shared
|
|
83
|
+
/// behind an `Arc` (not a mutex): all crate methods are `&self`, so ops just
|
|
84
|
+
/// share the handle. See the module docs for why no lock/latch is needed.
|
|
85
|
+
#[magnus::wrap(class = "QuietQUIC::Native::Connection", free_immediately, size)]
|
|
86
|
+
pub(crate) struct NativeConnection {
|
|
87
|
+
conn: Arc<CoreConnection>,
|
|
88
|
+
/// Cached at wrap time so `remote_address` is lock-free and always answers.
|
|
89
|
+
remote_addr: SocketAddr,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl NativeConnection {
|
|
93
|
+
/// Build from the crate `Connection` yielded by `accept` (server) or
|
|
94
|
+
/// `connect` (client). On the Ruby thread.
|
|
95
|
+
pub(crate) fn from_core(conn: CoreConnection) -> Self {
|
|
96
|
+
let remote_addr = conn.remote_address();
|
|
97
|
+
NativeConnection {
|
|
98
|
+
conn: Arc::new(conn),
|
|
99
|
+
remote_addr,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// `NativeConnection#remote_address -> String`. Synchronous, lock-free.
|
|
104
|
+
fn remote_address(&self) -> String {
|
|
105
|
+
self.remote_addr.to_string()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// `NativeConnection#open_stream_op -> PendingOp` (yields a `NativeStream`).
|
|
109
|
+
///
|
|
110
|
+
/// Opens a new bidirectional stream. `open_stream` is `&self`, so we share
|
|
111
|
+
/// the `Arc<Connection>` into the future with no lock. Yields the crate
|
|
112
|
+
/// `Stream` as plain Rust data; `to_ruby` wraps it into a `NativeStream` on
|
|
113
|
+
/// the Ruby thread.
|
|
114
|
+
fn open_stream_op(&self) -> Result<PendingOp, Error> {
|
|
115
|
+
let conn = Arc::clone(&self.conn);
|
|
116
|
+
PendingOp::spawn_op(
|
|
117
|
+
async move { conn.open_bi().await.map_err(map_conn_error) },
|
|
118
|
+
|stream: (CoreSendStream, CoreRecvStream)| {
|
|
119
|
+
let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
|
|
120
|
+
NativeStream::from_core(stream).into_value_with(&ruby)
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// `NativeConnection#accept_stream_op -> PendingOp` (yields a `NativeStream`).
|
|
126
|
+
///
|
|
127
|
+
/// Parks until the peer opens the next bidirectional stream. Holds NO lock
|
|
128
|
+
/// while parked (the crate driver holds the wait); a concurrent `close` on
|
|
129
|
+
/// this connection makes the crate driver fire this accept's reply with
|
|
130
|
+
/// `Closed`, so the future wakes and maps to `ConnectionLost` — no latch
|
|
131
|
+
/// needed on our side.
|
|
132
|
+
fn accept_stream_op(&self) -> Result<PendingOp, Error> {
|
|
133
|
+
let conn = Arc::clone(&self.conn);
|
|
134
|
+
PendingOp::spawn_op(
|
|
135
|
+
async move { conn.accept_bi().await.map_err(map_conn_error) },
|
|
136
|
+
|stream: (CoreSendStream, CoreRecvStream)| {
|
|
137
|
+
let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
|
|
138
|
+
NativeStream::from_core(stream).into_value_with(&ruby)
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// `NativeConnection#close`. Synchronous, lock-free, best-effort.
|
|
144
|
+
///
|
|
145
|
+
/// Sends a CONNECTION_CLOSE via the crate's `&self` `close`, so the peer and
|
|
146
|
+
/// this side's driver tear down promptly rather than idling out. The crate
|
|
147
|
+
/// `close` is `async`, but it only enqueues a `Close` command (a single
|
|
148
|
+
/// non-parking channel send) — we run it to completion on the shared runtime
|
|
149
|
+
/// with `block_on`. This runs on the Ruby thread and needs no lock, so it
|
|
150
|
+
/// can never deadlock behind a parked `accept_stream`/`read_to_end` (those
|
|
151
|
+
/// park in the crate driver, not behind a lock we hold). Idempotent: a
|
|
152
|
+
/// second close on an already-torn-down connection is a harmless no-op.
|
|
153
|
+
fn close(&self) {
|
|
154
|
+
let conn = Arc::clone(&self.conn);
|
|
155
|
+
crate::runtime::runtime().block_on(async move {
|
|
156
|
+
let _ = conn.close(0, b"").await;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// `QuietQUIC::Native::Stream` — TypedData wrapper over the crate's split
|
|
162
|
+
/// bidirectional send/receive stream halves.
|
|
163
|
+
///
|
|
164
|
+
/// The alpha.3 crate exposes `SendStream` and `RecvStream`; the Ruby API keeps
|
|
165
|
+
/// one `Stream` facade for compatibility and simplicity. Each half has its own
|
|
166
|
+
/// async mutex because the crate methods take `&mut self`: send operations
|
|
167
|
+
/// serialize with send operations, and receive operations serialize with
|
|
168
|
+
/// receive operations. A send and a receive on the same Ruby object do not
|
|
169
|
+
/// contend on one native lock.
|
|
170
|
+
#[magnus::wrap(class = "QuietQUIC::Native::Stream", free_immediately, size)]
|
|
171
|
+
pub(crate) struct NativeStream {
|
|
172
|
+
send: Arc<AsyncMutex<CoreSendStream>>,
|
|
173
|
+
recv: Arc<AsyncMutex<CoreRecvStream>>,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
impl NativeStream {
|
|
177
|
+
/// Wrap crate stream halves yielded by open/accept. On the Ruby thread.
|
|
178
|
+
fn from_core((send, recv): (CoreSendStream, CoreRecvStream)) -> Self {
|
|
179
|
+
NativeStream {
|
|
180
|
+
send: Arc::new(AsyncMutex::new(send)),
|
|
181
|
+
recv: Arc::new(AsyncMutex::new(recv)),
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// `NativeStream#write_all_op(bytes) -> PendingOp` (yields nil).
|
|
186
|
+
///
|
|
187
|
+
/// Reads the Ruby `String` as raw BYTES (`&[u8]`, encoding-agnostic) and
|
|
188
|
+
/// copies them out to an owned `Vec<u8>` synchronously on the Ruby thread —
|
|
189
|
+
/// BEFORE spawning — because the borrowed slice points at Ruby-owned memory
|
|
190
|
+
/// the GC may move/free once we yield. Writes the whole buffer, waiting out
|
|
191
|
+
/// flow-control back-pressure inside the crate driver.
|
|
192
|
+
fn write_all_op(&self, bytes: RString) -> Result<PendingOp, Error> {
|
|
193
|
+
// Copy the bytes out on the Ruby thread; the slice aliases Ruby memory,
|
|
194
|
+
// so we must not hold it across the spawn. `unsafe` is sound here: we do
|
|
195
|
+
// not re-enter Ruby or trigger GC between borrow and copy.
|
|
196
|
+
let data: Vec<u8> = unsafe { bytes.as_slice().to_vec() };
|
|
197
|
+
let send = Arc::clone(&self.send);
|
|
198
|
+
PendingOp::spawn_op(
|
|
199
|
+
async move {
|
|
200
|
+
let mut guard = send.lock().await;
|
|
201
|
+
guard.write_all(&data).await.map_err(map_conn_error)
|
|
202
|
+
},
|
|
203
|
+
|_unit: ()| {
|
|
204
|
+
let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
|
|
205
|
+
ruby.qnil().into_value_with(&ruby)
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// `NativeStream#finish_op -> PendingOp` (yields nil). Sends FIN.
|
|
211
|
+
fn finish_op(&self) -> Result<PendingOp, Error> {
|
|
212
|
+
let send = Arc::clone(&self.send);
|
|
213
|
+
PendingOp::spawn_op(
|
|
214
|
+
async move {
|
|
215
|
+
let mut guard = send.lock().await;
|
|
216
|
+
guard.finish().await.map_err(map_conn_error)
|
|
217
|
+
},
|
|
218
|
+
|_unit: ()| {
|
|
219
|
+
let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
|
|
220
|
+
ruby.qnil().into_value_with(&ruby)
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// `NativeStream#finish_and_wait_op(timeout_seconds) -> PendingOp` (yields nil).
|
|
226
|
+
fn finish_and_wait_op(&self, timeout_seconds: f64) -> Result<PendingOp, Error> {
|
|
227
|
+
let send = Arc::clone(&self.send);
|
|
228
|
+
PendingOp::spawn_op(
|
|
229
|
+
async move {
|
|
230
|
+
let mut guard = send.lock().await;
|
|
231
|
+
let duration = std::time::Duration::from_secs_f64(timeout_seconds);
|
|
232
|
+
tokio::time::timeout(duration, guard.finish_and_wait())
|
|
233
|
+
.await
|
|
234
|
+
.map_err(|_| MappedError::new(ErrorKind::Stream, "timed out waiting for stream FIN acknowledgement"))?
|
|
235
|
+
.map_err(map_conn_error)
|
|
236
|
+
},
|
|
237
|
+
|_unit: ()| {
|
|
238
|
+
let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
|
|
239
|
+
ruby.qnil().into_value_with(&ruby)
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// `NativeStream#read_to_end_op(limit) -> PendingOp` (yields a BINARY `String`).
|
|
245
|
+
///
|
|
246
|
+
/// Parks until the peer sends FIN, then yields all received bytes as plain
|
|
247
|
+
/// Rust `Vec<u8>`. `to_ruby` builds the Ruby `String` on the Ruby thread via
|
|
248
|
+
/// `Ruby::str_from_slice`, which sets `Encoding::BINARY` (ASCII-8BIT) — the
|
|
249
|
+
/// caller gets exact bytes, never a UTF-8-validated string. Holding the
|
|
250
|
+
/// receive-half async mutex across this parking await is safe (async mutex)
|
|
251
|
+
/// and does not interact with the send half or connection `close`.
|
|
252
|
+
fn read_to_end_op(&self, limit: usize) -> Result<PendingOp, Error> {
|
|
253
|
+
let recv = Arc::clone(&self.recv);
|
|
254
|
+
PendingOp::spawn_op(
|
|
255
|
+
async move {
|
|
256
|
+
let mut guard = recv.lock().await;
|
|
257
|
+
guard.read_to_end(limit).await.map_err(map_conn_error)
|
|
258
|
+
},
|
|
259
|
+
|bytes: Vec<u8>| {
|
|
260
|
+
let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
|
|
261
|
+
// BINARY-encoded String: exact bytes, no UTF-8 assumption.
|
|
262
|
+
ruby.str_from_slice(&bytes).into_value_with(&ruby)
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Register `Connection` + `Stream` and their methods on `QuietQUIC::Native`.
|
|
269
|
+
pub(crate) fn init(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
270
|
+
let connection = native.define_class("Connection", ruby.class_object())?;
|
|
271
|
+
connection.define_method("remote_address", method!(NativeConnection::remote_address, 0))?;
|
|
272
|
+
connection.define_method("open_stream_op", method!(NativeConnection::open_stream_op, 0))?;
|
|
273
|
+
connection.define_method("accept_stream_op", method!(NativeConnection::accept_stream_op, 0))?;
|
|
274
|
+
connection.define_method("close", method!(NativeConnection::close, 0))?;
|
|
275
|
+
|
|
276
|
+
let stream = native.define_class("Stream", ruby.class_object())?;
|
|
277
|
+
stream.define_method("write_all_op", method!(NativeStream::write_all_op, 1))?;
|
|
278
|
+
stream.define_method("finish_op", method!(NativeStream::finish_op, 0))?;
|
|
279
|
+
stream.define_method("finish_and_wait_op", method!(NativeStream::finish_and_wait_op, 1))?;
|
|
280
|
+
stream.define_method("read_to_end_op", method!(NativeStream::read_to_end_op, 1))?;
|
|
281
|
+
|
|
282
|
+
Ok(())
|
|
283
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
//! Rust-side model of quietquic's Ruby exception hierarchy and the mapping
|
|
3
|
+
//! from a spawned future's failure to the concrete `QuietQUIC::*` class.
|
|
4
|
+
//!
|
|
5
|
+
//! The exception classes are defined once at init and cached in a `OnceLock`
|
|
6
|
+
//! so that [`MappedError::into_ruby_error`] can look them up cheaply on the
|
|
7
|
+
//! Ruby thread. **No Ruby value is ever created on a tokio worker thread** —
|
|
8
|
+
//! a failing future yields a plain-data [`MappedError`], and the conversion to
|
|
9
|
+
//! a magnus `Error` (which carries a Ruby exception class) happens only inside
|
|
10
|
+
//! `PendingOp#take_result`, on the Ruby VM thread.
|
|
11
|
+
|
|
12
|
+
use std::sync::OnceLock;
|
|
13
|
+
|
|
14
|
+
use magnus::value::ReprValue;
|
|
15
|
+
use magnus::{prelude::*, value::Opaque, Error, ExceptionClass, RModule, Ruby};
|
|
16
|
+
|
|
17
|
+
/// The subset of the exception hierarchy a mapped error can select. Each
|
|
18
|
+
/// variant corresponds 1:1 to a class defined under `QuietQUIC`.
|
|
19
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
20
|
+
pub(crate) enum ErrorKind {
|
|
21
|
+
/// `QuietQUIC::Error` — the base class (also the fallback).
|
|
22
|
+
Base,
|
|
23
|
+
/// `QuietQUIC::ConfigError`.
|
|
24
|
+
Config,
|
|
25
|
+
/// `QuietQUIC::ConnectError`.
|
|
26
|
+
Connect,
|
|
27
|
+
/// `QuietQUIC::ConnectionLost`.
|
|
28
|
+
ConnectionLost,
|
|
29
|
+
/// `QuietQUIC::StreamError`.
|
|
30
|
+
Stream,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl ErrorKind {
|
|
34
|
+
/// Parse a lowercase kind string (used by the `fail_op` test helper and,
|
|
35
|
+
/// later, by internal call sites that classify core errors by name).
|
|
36
|
+
pub(crate) fn from_str(kind: &str) -> Self {
|
|
37
|
+
match kind {
|
|
38
|
+
"config" => ErrorKind::Config,
|
|
39
|
+
"connect" => ErrorKind::Connect,
|
|
40
|
+
"connection_lost" => ErrorKind::ConnectionLost,
|
|
41
|
+
"stream" => ErrorKind::Stream,
|
|
42
|
+
_ => ErrorKind::Base,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// A failure produced by a spawned future. Pure Rust data — safe to move
|
|
48
|
+
/// across the tokio/Ruby thread boundary. Converted to a magnus `Error`
|
|
49
|
+
/// (with a real Ruby exception class) only on the Ruby thread.
|
|
50
|
+
#[derive(Debug, Clone)]
|
|
51
|
+
pub(crate) struct MappedError {
|
|
52
|
+
pub(crate) kind: ErrorKind,
|
|
53
|
+
pub(crate) message: String,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl MappedError {
|
|
57
|
+
pub(crate) fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
|
|
58
|
+
MappedError {
|
|
59
|
+
kind,
|
|
60
|
+
message: message.into(),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Build the magnus `Error` carrying the concrete `QuietQUIC::*` class.
|
|
65
|
+
/// Must be called on the Ruby thread (it dereferences cached class handles
|
|
66
|
+
/// and constructs an exception).
|
|
67
|
+
pub(crate) fn into_ruby_error(self, ruby: &Ruby) -> Error {
|
|
68
|
+
let class = exception_class(ruby, self.kind);
|
|
69
|
+
Error::new(class, self.message)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Cached exception classes, populated at init on the Ruby thread. `Opaque`
|
|
74
|
+
/// makes the handles `Send` so they can live in the process-global `OnceLock`;
|
|
75
|
+
/// they are only ever *dereferenced* back on a Ruby thread via `ruby.get_inner`.
|
|
76
|
+
struct Classes {
|
|
77
|
+
base: Opaque<ExceptionClass>,
|
|
78
|
+
config: Opaque<ExceptionClass>,
|
|
79
|
+
connect: Opaque<ExceptionClass>,
|
|
80
|
+
connection_lost: Opaque<ExceptionClass>,
|
|
81
|
+
stream: Opaque<ExceptionClass>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static CLASSES: OnceLock<Classes> = OnceLock::new();
|
|
85
|
+
|
|
86
|
+
/// The `QuietQUIC::Error` base exception class, resolved on the Ruby thread.
|
|
87
|
+
/// Used by call sites (e.g. the fork guard) that raise the base error directly
|
|
88
|
+
/// without going through a [`MappedError`].
|
|
89
|
+
pub(crate) fn base_error_class(ruby: &Ruby) -> ExceptionClass {
|
|
90
|
+
exception_class(ruby, ErrorKind::Base)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Resolve the concrete `ExceptionClass` for a kind, on the Ruby thread.
|
|
94
|
+
fn exception_class(ruby: &Ruby, kind: ErrorKind) -> ExceptionClass {
|
|
95
|
+
let classes = CLASSES
|
|
96
|
+
.get()
|
|
97
|
+
.expect("errors::init must run before exception_class");
|
|
98
|
+
let opaque = match kind {
|
|
99
|
+
ErrorKind::Base => classes.base,
|
|
100
|
+
ErrorKind::Config => classes.config,
|
|
101
|
+
ErrorKind::Connect => classes.connect,
|
|
102
|
+
ErrorKind::ConnectionLost => classes.connection_lost,
|
|
103
|
+
ErrorKind::Stream => classes.stream,
|
|
104
|
+
};
|
|
105
|
+
ruby.get_inner(opaque)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Define the exception hierarchy under `QuietQUIC` and cache the classes.
|
|
109
|
+
///
|
|
110
|
+
/// `Error < StandardError`; `ConfigError`, `ConnectError`, `ConnectionLost`,
|
|
111
|
+
/// and `StreamError` all `< QuietQUIC::Error`.
|
|
112
|
+
pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
113
|
+
let base = module.define_class("Error", ruby.exception_standard_error().as_r_class())?;
|
|
114
|
+
let base_exc = ExceptionClass::from_value(base.as_value())
|
|
115
|
+
.expect("QuietQUIC::Error is an exception class");
|
|
116
|
+
|
|
117
|
+
let define = |name: &str| -> Result<Opaque<ExceptionClass>, Error> {
|
|
118
|
+
let klass = module.define_class(name, base_exc.as_r_class())?;
|
|
119
|
+
let exc = ExceptionClass::from_value(klass.as_value())
|
|
120
|
+
.expect("subclass of QuietQUIC::Error is an exception class");
|
|
121
|
+
Ok(Opaque::from(exc))
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let config = define("ConfigError")?;
|
|
125
|
+
let connect = define("ConnectError")?;
|
|
126
|
+
let connection_lost = define("ConnectionLost")?;
|
|
127
|
+
let stream = define("StreamError")?;
|
|
128
|
+
|
|
129
|
+
CLASSES
|
|
130
|
+
.set(Classes {
|
|
131
|
+
base: Opaque::from(base_exc),
|
|
132
|
+
config,
|
|
133
|
+
connect,
|
|
134
|
+
connection_lost,
|
|
135
|
+
stream,
|
|
136
|
+
})
|
|
137
|
+
.ok();
|
|
138
|
+
|
|
139
|
+
Ok(())
|
|
140
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
use magnus::{function, prelude::*, Error, Ruby};
|
|
3
|
+
|
|
4
|
+
mod client;
|
|
5
|
+
mod config;
|
|
6
|
+
mod conn;
|
|
7
|
+
mod errors;
|
|
8
|
+
mod pending;
|
|
9
|
+
mod runtime;
|
|
10
|
+
mod server;
|
|
11
|
+
|
|
12
|
+
fn ping() -> &'static str {
|
|
13
|
+
"pong"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// `QuietQUIC::Native.sleep_op(millis) -> PendingOp`.
|
|
17
|
+
fn sleep_op(millis: u64) -> Result<pending::PendingOp, Error> {
|
|
18
|
+
pending::sleep_op(millis)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// `QuietQUIC::Native.echo_op(str) -> PendingOp`.
|
|
22
|
+
fn echo_op(s: String) -> Result<pending::PendingOp, Error> {
|
|
23
|
+
pending::echo_op(s)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// `QuietQUIC::Native.nil_op -> PendingOp`.
|
|
27
|
+
fn nil_op() -> Result<pending::PendingOp, Error> {
|
|
28
|
+
pending::nil_op()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// `QuietQUIC::Native.fail_op(kind) -> PendingOp` (test helper).
|
|
32
|
+
fn fail_op(kind: String) -> Result<pending::PendingOp, Error> {
|
|
33
|
+
pending::fail_op(kind)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// `QuietQUIC::Native.panic_op -> PendingOp` (dev/test helper).
|
|
37
|
+
fn panic_op() -> Result<pending::PendingOp, Error> {
|
|
38
|
+
pending::panic_op()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[magnus::init]
|
|
42
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
43
|
+
let module = ruby.define_module("QuietQUIC")?;
|
|
44
|
+
errors::init(ruby, &module)?;
|
|
45
|
+
|
|
46
|
+
let native = module.define_module("Native")?;
|
|
47
|
+
native.define_singleton_method("ping", function!(ping, 0))?;
|
|
48
|
+
native.define_singleton_method("sleep_op", function!(sleep_op, 1))?;
|
|
49
|
+
native.define_singleton_method("echo_op", function!(echo_op, 1))?;
|
|
50
|
+
native.define_singleton_method("nil_op", function!(nil_op, 0))?;
|
|
51
|
+
native.define_singleton_method("fail_op", function!(fail_op, 1))?;
|
|
52
|
+
native.define_singleton_method("panic_op", function!(panic_op, 0))?;
|
|
53
|
+
pending::init(ruby, &native)?;
|
|
54
|
+
config::init(ruby, &native)?;
|
|
55
|
+
conn::init(ruby, &native)?;
|
|
56
|
+
server::init(ruby, &native)?;
|
|
57
|
+
client::init(ruby, &native)?;
|
|
58
|
+
Ok(())
|
|
59
|
+
}
|