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.
@@ -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
+ }