hyperion-rb 1.6.2 → 2.11.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4768 -0
- data/README.md +222 -13
- data/ext/hyperion_h2_codec/Cargo.lock +7 -0
- data/ext/hyperion_h2_codec/Cargo.toml +33 -0
- data/ext/hyperion_h2_codec/extconf.rb +73 -0
- data/ext/hyperion_h2_codec/src/frames.rs +140 -0
- data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
- data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
- data/ext/hyperion_h2_codec/src/lib.rs +296 -0
- data/ext/hyperion_http/extconf.rb +28 -0
- data/ext/hyperion_http/h2_codec_glue.c +408 -0
- data/ext/hyperion_http/page_cache.c +1125 -0
- data/ext/hyperion_http/parser.c +473 -38
- data/ext/hyperion_http/sendfile.c +982 -0
- data/ext/hyperion_http/websocket.c +493 -0
- data/ext/hyperion_io_uring/Cargo.lock +33 -0
- data/ext/hyperion_io_uring/Cargo.toml +34 -0
- data/ext/hyperion_io_uring/extconf.rb +74 -0
- data/ext/hyperion_io_uring/src/lib.rs +316 -0
- data/lib/hyperion/adapter/rack.rb +370 -42
- data/lib/hyperion/admin_listener.rb +207 -0
- data/lib/hyperion/admin_middleware.rb +36 -7
- data/lib/hyperion/cli.rb +310 -11
- data/lib/hyperion/config.rb +440 -14
- data/lib/hyperion/connection.rb +679 -22
- data/lib/hyperion/deprecations.rb +81 -0
- data/lib/hyperion/dispatch_mode.rb +165 -0
- data/lib/hyperion/fiber_local.rb +75 -13
- data/lib/hyperion/h2_admission.rb +77 -0
- data/lib/hyperion/h2_codec.rb +499 -0
- data/lib/hyperion/http/page_cache.rb +122 -0
- data/lib/hyperion/http/sendfile.rb +696 -0
- data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
- data/lib/hyperion/http2_handler.rb +618 -19
- data/lib/hyperion/io_uring.rb +317 -0
- data/lib/hyperion/lint_wrapper_pool.rb +126 -0
- data/lib/hyperion/master.rb +96 -9
- data/lib/hyperion/metrics/path_templater.rb +68 -0
- data/lib/hyperion/metrics.rb +256 -0
- data/lib/hyperion/prometheus_exporter.rb +150 -0
- data/lib/hyperion/request.rb +13 -0
- data/lib/hyperion/response_writer.rb +477 -16
- data/lib/hyperion/runtime.rb +195 -0
- data/lib/hyperion/server/route_table.rb +179 -0
- data/lib/hyperion/server.rb +519 -55
- data/lib/hyperion/static_preload.rb +133 -0
- data/lib/hyperion/thread_pool.rb +61 -7
- data/lib/hyperion/tls.rb +343 -1
- data/lib/hyperion/version.rb +1 -1
- data/lib/hyperion/websocket/close_codes.rb +71 -0
- data/lib/hyperion/websocket/connection.rb +876 -0
- data/lib/hyperion/websocket/frame.rb +356 -0
- data/lib/hyperion/websocket/handshake.rb +525 -0
- data/lib/hyperion/worker.rb +111 -9
- data/lib/hyperion.rb +137 -3
- metadata +50 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
//! Hyperion HTTP/2 codec — Rust core, exposed to Ruby via extern "C".
|
|
2
|
+
//!
|
|
3
|
+
//! Phase 6a (HPACK encoder/decoder) is what 2.0.0 ships. Phase 6b
|
|
4
|
+
//! (frame ser/de) is wired through `frames.rs` but the integration
|
|
5
|
+
//! into `Http2Handler` reuses `protocol-http2`'s framer for now —
|
|
6
|
+
//! frame ser/de is microsecond-scale and not the bottleneck the bench
|
|
7
|
+
//! is calling out. HPACK encode is ~70% of the per-stream encoder
|
|
8
|
+
//! cost in the 1.x ruby-only path.
|
|
9
|
+
//!
|
|
10
|
+
//! The Ruby side calls these via Fiddle (see lib/hyperion/h2_codec.rb).
|
|
11
|
+
//! Memory model: encoders/decoders are owned Box pointers; Ruby holds
|
|
12
|
+
//! an opaque `void*` and explicitly frees via `*_free`. All byte
|
|
13
|
+
//! payloads are copied across the boundary — no shared mutable state.
|
|
14
|
+
|
|
15
|
+
#![allow(clippy::missing_safety_doc)]
|
|
16
|
+
|
|
17
|
+
mod hpack;
|
|
18
|
+
mod frames;
|
|
19
|
+
|
|
20
|
+
use std::os::raw::{c_int, c_uchar};
|
|
21
|
+
use std::os::raw::c_longlong;
|
|
22
|
+
|
|
23
|
+
/// Opaque handle exported to Ruby.
|
|
24
|
+
type EncoderHandle = *mut hpack::Encoder;
|
|
25
|
+
/// Opaque handle exported to Ruby.
|
|
26
|
+
type DecoderHandle = *mut hpack::Decoder;
|
|
27
|
+
|
|
28
|
+
// ---------- Encoder ----------
|
|
29
|
+
|
|
30
|
+
/// Allocate a new HPACK encoder. Returned pointer must be freed by
|
|
31
|
+
/// `hyperion_h2_codec_encoder_free`.
|
|
32
|
+
#[no_mangle]
|
|
33
|
+
pub extern "C" fn hyperion_h2_codec_encoder_new() -> EncoderHandle {
|
|
34
|
+
Box::into_raw(Box::new(hpack::Encoder::new()))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Free an encoder handle.
|
|
38
|
+
#[no_mangle]
|
|
39
|
+
pub unsafe extern "C" fn hyperion_h2_codec_encoder_free(ptr: EncoderHandle) {
|
|
40
|
+
if !ptr.is_null() {
|
|
41
|
+
drop(Box::from_raw(ptr));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Encode a header block. Inputs:
|
|
46
|
+
/// names_ptr/lens_ptr/count: parallel arrays describing the names
|
|
47
|
+
/// vals_ptr/vlens_ptr : parallel arrays describing the values
|
|
48
|
+
/// out_ptr / out_capacity : caller-provided output buffer
|
|
49
|
+
///
|
|
50
|
+
/// Returns the number of bytes written, or -1 on overflow / -2 on bad
|
|
51
|
+
/// arguments.
|
|
52
|
+
#[no_mangle]
|
|
53
|
+
pub unsafe extern "C" fn hyperion_h2_codec_encoder_encode(
|
|
54
|
+
handle: EncoderHandle,
|
|
55
|
+
names_ptr: *const *const c_uchar,
|
|
56
|
+
name_lens: *const u32,
|
|
57
|
+
vals_ptr: *const *const c_uchar,
|
|
58
|
+
val_lens: *const u32,
|
|
59
|
+
count: u32,
|
|
60
|
+
out_ptr: *mut c_uchar,
|
|
61
|
+
out_capacity: u32,
|
|
62
|
+
) -> c_int {
|
|
63
|
+
if handle.is_null() || out_ptr.is_null() {
|
|
64
|
+
return -2;
|
|
65
|
+
}
|
|
66
|
+
let encoder = &mut *handle;
|
|
67
|
+
let mut buf = Vec::with_capacity(64 * count as usize);
|
|
68
|
+
|
|
69
|
+
for i in 0..count as isize {
|
|
70
|
+
let name_p = *names_ptr.offset(i);
|
|
71
|
+
let name_l = *name_lens.offset(i) as usize;
|
|
72
|
+
let val_p = *vals_ptr.offset(i);
|
|
73
|
+
let val_l = *val_lens.offset(i) as usize;
|
|
74
|
+
if name_p.is_null() || (val_p.is_null() && val_l > 0) {
|
|
75
|
+
return -2;
|
|
76
|
+
}
|
|
77
|
+
let name = std::slice::from_raw_parts(name_p, name_l);
|
|
78
|
+
let value = std::slice::from_raw_parts(val_p, val_l);
|
|
79
|
+
encoder.encode_header(name, value, &mut buf);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if buf.len() > out_capacity as usize {
|
|
83
|
+
return -1;
|
|
84
|
+
}
|
|
85
|
+
std::ptr::copy_nonoverlapping(buf.as_ptr(), out_ptr, buf.len());
|
|
86
|
+
buf.len() as c_int
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// fix-B (2.2.x) — flat-blob encode ABI. Eliminates the per-header
|
|
90
|
+
/// allocation profile of the v1 entry point: callers pack the
|
|
91
|
+
/// concatenated names/values into ONE byte blob and a parallel array
|
|
92
|
+
/// of `(name_off, name_len, value_off, value_len)` u64 quads in
|
|
93
|
+
/// `argv_ptr`. The Rust side indexes into the blob via offsets — no
|
|
94
|
+
/// per-header `Fiddle::Pointer.new` on the Ruby side, no per-header
|
|
95
|
+
/// `pack('Q*')`.
|
|
96
|
+
///
|
|
97
|
+
/// Inputs:
|
|
98
|
+
/// * `handle` — encoder handle (preserved across calls; dyn table state)
|
|
99
|
+
/// * `headers_blob_ptr` — concatenated bytes (name_1, value_1, name_2, value_2, …)
|
|
100
|
+
/// * `headers_blob_len` — length of the blob in bytes
|
|
101
|
+
/// * `argv_ptr` — pointer to `argv_count * 4` little-endian u64s
|
|
102
|
+
/// * `argv_count` — number of header pairs
|
|
103
|
+
/// * `out_ptr` — caller-provided output buffer
|
|
104
|
+
/// * `out_capacity` — bytes available at `out_ptr`
|
|
105
|
+
///
|
|
106
|
+
/// Returns the number of bytes written, or:
|
|
107
|
+
/// -1 = output buffer overflow
|
|
108
|
+
/// -2 = bad arguments (null pointer, offset/length out of blob bounds)
|
|
109
|
+
///
|
|
110
|
+
/// Old `hyperion_h2_codec_encoder_encode` ABI symbol is **preserved
|
|
111
|
+
/// unchanged** above for backwards compatibility — older Ruby loaders
|
|
112
|
+
/// that still call it continue to work; the in-tree Ruby wrapper
|
|
113
|
+
/// switches to v2 at the FFI boundary.
|
|
114
|
+
#[no_mangle]
|
|
115
|
+
pub unsafe extern "C" fn hyperion_h2_codec_encoder_encode_v2(
|
|
116
|
+
handle: EncoderHandle,
|
|
117
|
+
headers_blob_ptr: *const c_uchar,
|
|
118
|
+
headers_blob_len: usize,
|
|
119
|
+
argv_ptr: *const u64,
|
|
120
|
+
argv_count: usize,
|
|
121
|
+
out_ptr: *mut c_uchar,
|
|
122
|
+
out_capacity: usize,
|
|
123
|
+
) -> c_longlong {
|
|
124
|
+
if handle.is_null() || out_ptr.is_null() {
|
|
125
|
+
return -2;
|
|
126
|
+
}
|
|
127
|
+
if argv_count > 0 && argv_ptr.is_null() {
|
|
128
|
+
return -2;
|
|
129
|
+
}
|
|
130
|
+
if headers_blob_len > 0 && headers_blob_ptr.is_null() {
|
|
131
|
+
return -2;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let encoder = &mut *handle;
|
|
135
|
+
// Reuse the per-encoder scratch buffer instead of allocating a
|
|
136
|
+
// fresh `Vec::with_capacity(64 * count)` per call. `clear()`
|
|
137
|
+
// length-zeros without dropping the backing allocation.
|
|
138
|
+
encoder.scratch.clear();
|
|
139
|
+
|
|
140
|
+
let blob: &[u8] = if headers_blob_len == 0 {
|
|
141
|
+
&[]
|
|
142
|
+
} else {
|
|
143
|
+
std::slice::from_raw_parts(headers_blob_ptr, headers_blob_len)
|
|
144
|
+
};
|
|
145
|
+
let argv: &[u64] = if argv_count == 0 {
|
|
146
|
+
&[]
|
|
147
|
+
} else {
|
|
148
|
+
std::slice::from_raw_parts(argv_ptr, argv_count.saturating_mul(4))
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Move the scratch out of the encoder while we encode (so the
|
|
152
|
+
// borrow checker lets us call &mut self methods that touch the
|
|
153
|
+
// dyn table). This is a value swap — no allocation. We swap it
|
|
154
|
+
// back at the end so the next call reuses the same allocation.
|
|
155
|
+
let mut scratch = std::mem::take(&mut encoder.scratch);
|
|
156
|
+
|
|
157
|
+
for i in 0..argv_count {
|
|
158
|
+
let base = i.saturating_mul(4);
|
|
159
|
+
let name_off = argv[base] as usize;
|
|
160
|
+
let name_len = argv[base + 1] as usize;
|
|
161
|
+
let val_off = argv[base + 2] as usize;
|
|
162
|
+
let val_len = argv[base + 3] as usize;
|
|
163
|
+
if name_off
|
|
164
|
+
.checked_add(name_len)
|
|
165
|
+
.map(|end| end > blob.len())
|
|
166
|
+
.unwrap_or(true)
|
|
167
|
+
{
|
|
168
|
+
encoder.scratch = scratch;
|
|
169
|
+
return -2;
|
|
170
|
+
}
|
|
171
|
+
if val_off
|
|
172
|
+
.checked_add(val_len)
|
|
173
|
+
.map(|end| end > blob.len())
|
|
174
|
+
.unwrap_or(true)
|
|
175
|
+
{
|
|
176
|
+
encoder.scratch = scratch;
|
|
177
|
+
return -2;
|
|
178
|
+
}
|
|
179
|
+
let name = &blob[name_off..name_off + name_len];
|
|
180
|
+
let value = &blob[val_off..val_off + val_len];
|
|
181
|
+
encoder.encode_header(name, value, &mut scratch);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let written = scratch.len();
|
|
185
|
+
let result: c_longlong = if written > out_capacity {
|
|
186
|
+
-1
|
|
187
|
+
} else {
|
|
188
|
+
std::ptr::copy_nonoverlapping(scratch.as_ptr(), out_ptr, written);
|
|
189
|
+
written as c_longlong
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Restore the scratch (cleared on next call) so the allocation persists.
|
|
193
|
+
encoder.scratch = scratch;
|
|
194
|
+
result
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------- Decoder ----------
|
|
198
|
+
|
|
199
|
+
#[no_mangle]
|
|
200
|
+
pub extern "C" fn hyperion_h2_codec_decoder_new() -> DecoderHandle {
|
|
201
|
+
Box::into_raw(Box::new(hpack::Decoder::new()))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[no_mangle]
|
|
205
|
+
pub unsafe extern "C" fn hyperion_h2_codec_decoder_free(ptr: DecoderHandle) {
|
|
206
|
+
if !ptr.is_null() {
|
|
207
|
+
drop(Box::from_raw(ptr));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Decode a header block. Output format: a flat byte buffer of
|
|
212
|
+
/// [u32 name_len][name bytes][u32 value_len][value bytes]
|
|
213
|
+
/// repeated. Returns total bytes written, or:
|
|
214
|
+
/// -1 = output buffer overflow
|
|
215
|
+
/// -2 = bad arguments
|
|
216
|
+
/// -3 = malformed HPACK input
|
|
217
|
+
#[no_mangle]
|
|
218
|
+
pub unsafe extern "C" fn hyperion_h2_codec_decoder_decode(
|
|
219
|
+
handle: DecoderHandle,
|
|
220
|
+
in_ptr: *const c_uchar,
|
|
221
|
+
in_len: u32,
|
|
222
|
+
out_ptr: *mut c_uchar,
|
|
223
|
+
out_capacity: u32,
|
|
224
|
+
) -> c_int {
|
|
225
|
+
if handle.is_null() || in_ptr.is_null() || out_ptr.is_null() {
|
|
226
|
+
return -2;
|
|
227
|
+
}
|
|
228
|
+
let decoder = &mut *handle;
|
|
229
|
+
let input = std::slice::from_raw_parts(in_ptr, in_len as usize);
|
|
230
|
+
let headers = match decoder.decode(input) {
|
|
231
|
+
Ok(h) => h,
|
|
232
|
+
Err(_) => return -3,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let mut needed: usize = 0;
|
|
236
|
+
for (n, v) in &headers {
|
|
237
|
+
needed = needed.saturating_add(8 + n.len() + v.len());
|
|
238
|
+
}
|
|
239
|
+
if needed > out_capacity as usize {
|
|
240
|
+
return -1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let out = std::slice::from_raw_parts_mut(out_ptr, needed);
|
|
244
|
+
let mut off = 0usize;
|
|
245
|
+
for (n, v) in &headers {
|
|
246
|
+
let nl = (n.len() as u32).to_le_bytes();
|
|
247
|
+
out[off..off + 4].copy_from_slice(&nl);
|
|
248
|
+
off += 4;
|
|
249
|
+
out[off..off + n.len()].copy_from_slice(n);
|
|
250
|
+
off += n.len();
|
|
251
|
+
let vl = (v.len() as u32).to_le_bytes();
|
|
252
|
+
out[off..off + 4].copy_from_slice(&vl);
|
|
253
|
+
off += 4;
|
|
254
|
+
out[off..off + v.len()].copy_from_slice(v);
|
|
255
|
+
off += v.len();
|
|
256
|
+
}
|
|
257
|
+
needed as c_int
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------- Frame primitives (Phase 6b stub) ----------
|
|
261
|
+
//
|
|
262
|
+
// `frames.rs` is exposed for completeness and self-test; the
|
|
263
|
+
// production handler still drives `protocol-http2`'s framer for the
|
|
264
|
+
// connection state machine. When the bench shows frame ser/de is the
|
|
265
|
+
// next bottleneck we'll wire these in.
|
|
266
|
+
|
|
267
|
+
#[no_mangle]
|
|
268
|
+
pub unsafe extern "C" fn hyperion_h2_codec_encode_data_frame(
|
|
269
|
+
stream_id: u32,
|
|
270
|
+
end_stream: c_int,
|
|
271
|
+
payload_ptr: *const c_uchar,
|
|
272
|
+
payload_len: u32,
|
|
273
|
+
out_ptr: *mut c_uchar,
|
|
274
|
+
out_capacity: u32,
|
|
275
|
+
) -> c_int {
|
|
276
|
+
if payload_ptr.is_null() || out_ptr.is_null() {
|
|
277
|
+
return -2;
|
|
278
|
+
}
|
|
279
|
+
let payload = std::slice::from_raw_parts(payload_ptr, payload_len as usize);
|
|
280
|
+
let frame =
|
|
281
|
+
frames::encode_data_frame(stream_id, end_stream != 0, payload);
|
|
282
|
+
if frame.len() > out_capacity as usize {
|
|
283
|
+
return -1;
|
|
284
|
+
}
|
|
285
|
+
std::ptr::copy_nonoverlapping(frame.as_ptr(), out_ptr, frame.len());
|
|
286
|
+
frame.len() as c_int
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Smoke test entry — Ruby calls this in `available?` to confirm the
|
|
290
|
+
/// shared library loaded and the symbols resolve correctly. Returns
|
|
291
|
+
/// the codec ABI version (incremented on any breaking ABI change so
|
|
292
|
+
/// Ruby can refuse to load mismatched binaries).
|
|
293
|
+
#[no_mangle]
|
|
294
|
+
pub extern "C" fn hyperion_h2_codec_abi_version() -> u32 {
|
|
295
|
+
1
|
|
296
|
+
}
|
|
@@ -6,8 +6,19 @@ require 'mkmf'
|
|
|
6
6
|
# We compile them in-tree (single static unit) rather than linking a
|
|
7
7
|
# system library, so the gem builds standalone on any host with a C
|
|
8
8
|
# compiler + Ruby headers.
|
|
9
|
+
#
|
|
10
|
+
# 1.7.0 Phase 1 adds sendfile.c — a sibling translation unit that owns
|
|
11
|
+
# Hyperion::Http::Sendfile. Linked into the same .bundle/.so as parser.c
|
|
12
|
+
# (single `require` for both), with platform-specific kernel calls
|
|
13
|
+
# guarded inside the C source rather than at extconf time so we still
|
|
14
|
+
# build cleanly on platforms where neither sendfile(2) nor splice(2)
|
|
15
|
+
# is available (the C source raises NotImplementedError at call time).
|
|
9
16
|
$srcs = %w[
|
|
10
17
|
parser.c
|
|
18
|
+
sendfile.c
|
|
19
|
+
page_cache.c
|
|
20
|
+
websocket.c
|
|
21
|
+
h2_codec_glue.c
|
|
11
22
|
llhttp.c
|
|
12
23
|
api.c
|
|
13
24
|
http.c
|
|
@@ -16,4 +27,21 @@ $VPATH << '$(srcdir)/llhttp'
|
|
|
16
27
|
$INCFLAGS << ' -I$(srcdir)/llhttp'
|
|
17
28
|
$CFLAGS << ' -O2 -fno-strict-aliasing'
|
|
18
29
|
|
|
30
|
+
# Probe for sendfile/splice headers so the C source can use the right
|
|
31
|
+
# branch with #ifdef. mkmf's have_header writes -DHAVE_<NAME> macros
|
|
32
|
+
# that sendfile.c can consult if we want a finer split later; for now
|
|
33
|
+
# the source already picks the path off __linux__ / __APPLE__ /
|
|
34
|
+
# __FreeBSD__ defines that the toolchain provides automatically.
|
|
35
|
+
have_header('sys/sendfile.h') # Linux: sendfile64
|
|
36
|
+
have_header('sys/uio.h') # BSD/Darwin sendfile + Linux iovec plumbing
|
|
37
|
+
have_header('sys/socket.h')
|
|
38
|
+
|
|
39
|
+
# 2.4-A: h2_codec_glue.c calls dlopen/dlsym to wire the Rust HPACK
|
|
40
|
+
# cdylib without going through Fiddle on the per-call hot path. macOS
|
|
41
|
+
# ships dlopen in libSystem (no extra link flag needed); Linux glibc
|
|
42
|
+
# requires `-ldl` (musl rolls dlopen into libc, but `have_library`
|
|
43
|
+
# returns true on both because the symbol resolves either way).
|
|
44
|
+
have_header('dlfcn.h')
|
|
45
|
+
have_library('dl', 'dlopen')
|
|
46
|
+
|
|
19
47
|
create_makefile('hyperion_http/hyperion_http')
|