hyperion-rb 1.6.2 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4563 -0
  3. data/README.md +189 -13
  4. data/ext/hyperion_h2_codec/Cargo.lock +7 -0
  5. data/ext/hyperion_h2_codec/Cargo.toml +33 -0
  6. data/ext/hyperion_h2_codec/extconf.rb +73 -0
  7. data/ext/hyperion_h2_codec/src/frames.rs +140 -0
  8. data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
  9. data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
  10. data/ext/hyperion_h2_codec/src/lib.rs +296 -0
  11. data/ext/hyperion_http/extconf.rb +28 -0
  12. data/ext/hyperion_http/h2_codec_glue.c +408 -0
  13. data/ext/hyperion_http/page_cache.c +1125 -0
  14. data/ext/hyperion_http/parser.c +473 -38
  15. data/ext/hyperion_http/sendfile.c +982 -0
  16. data/ext/hyperion_http/websocket.c +493 -0
  17. data/ext/hyperion_io_uring/Cargo.lock +33 -0
  18. data/ext/hyperion_io_uring/Cargo.toml +34 -0
  19. data/ext/hyperion_io_uring/extconf.rb +74 -0
  20. data/ext/hyperion_io_uring/src/lib.rs +316 -0
  21. data/lib/hyperion/adapter/rack.rb +370 -42
  22. data/lib/hyperion/admin_listener.rb +207 -0
  23. data/lib/hyperion/admin_middleware.rb +36 -7
  24. data/lib/hyperion/cli.rb +310 -11
  25. data/lib/hyperion/config.rb +440 -14
  26. data/lib/hyperion/connection.rb +679 -22
  27. data/lib/hyperion/deprecations.rb +81 -0
  28. data/lib/hyperion/dispatch_mode.rb +165 -0
  29. data/lib/hyperion/fiber_local.rb +75 -13
  30. data/lib/hyperion/h2_admission.rb +77 -0
  31. data/lib/hyperion/h2_codec.rb +452 -0
  32. data/lib/hyperion/http/page_cache.rb +122 -0
  33. data/lib/hyperion/http/sendfile.rb +696 -0
  34. data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
  35. data/lib/hyperion/http2_handler.rb +368 -9
  36. data/lib/hyperion/io_uring.rb +317 -0
  37. data/lib/hyperion/lint_wrapper_pool.rb +126 -0
  38. data/lib/hyperion/master.rb +96 -9
  39. data/lib/hyperion/metrics/path_templater.rb +68 -0
  40. data/lib/hyperion/metrics.rb +256 -0
  41. data/lib/hyperion/prometheus_exporter.rb +150 -0
  42. data/lib/hyperion/request.rb +13 -0
  43. data/lib/hyperion/response_writer.rb +477 -16
  44. data/lib/hyperion/runtime.rb +195 -0
  45. data/lib/hyperion/server/route_table.rb +179 -0
  46. data/lib/hyperion/server.rb +519 -55
  47. data/lib/hyperion/static_preload.rb +133 -0
  48. data/lib/hyperion/thread_pool.rb +61 -7
  49. data/lib/hyperion/tls.rb +343 -1
  50. data/lib/hyperion/version.rb +1 -1
  51. data/lib/hyperion/websocket/close_codes.rb +71 -0
  52. data/lib/hyperion/websocket/connection.rb +876 -0
  53. data/lib/hyperion/websocket/frame.rb +356 -0
  54. data/lib/hyperion/websocket/handshake.rb +525 -0
  55. data/lib/hyperion/worker.rb +111 -9
  56. data/lib/hyperion.rb +137 -3
  57. 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')