Module: Hyperion::H2Codec::CGlue

Defined in:
ext/hyperion_http/h2_codec_glue.c

Class Method Summary collapse

Class Method Details

.available?Boolean

Returns:

  • (Boolean)


155
156
157
158
# File 'ext/hyperion_http/h2_codec_glue.c', line 155

static VALUE rb_cglue_available_p(VALUE self) {
    (void)self;
    return cglue_available ? Qtrue : Qfalse;
}

.decoder_decode_v3(rb_handle_addr, rb_bytes, rb_scratch_out) ⇒ Object




321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'ext/hyperion_http/h2_codec_glue.c', line 321

static VALUE rb_cglue_decoder_decode_v3(VALUE self,
                                        VALUE rb_handle_addr,
                                        VALUE rb_bytes,
                                        VALUE rb_scratch_out) {
    (void)self;

    if (!cglue_available || !rust_decode) {
        rb_raise(rb_eRuntimeError,
                 "Hyperion::H2Codec::CGlue not installed (call .install(path) first)");
    }

    Check_Type(rb_bytes, T_STRING);
    Check_Type(rb_scratch_out, T_STRING);

    void *handle = (void *)(intptr_t)NUM2LL(rb_handle_addr);

    long in_len = RSTRING_LEN(rb_bytes);
    if (in_len == 0) {
        rb_str_set_len(rb_scratch_out, 0);
        return INT2FIX(0);
    }

    rb_str_modify(rb_scratch_out);
    long out_capacity = RSTRING_LEN(rb_scratch_out);

    int written = rust_decode(handle,
                              (const unsigned char *)RSTRING_PTR(rb_bytes),
                              (unsigned int)in_len,
                              (unsigned char *)RSTRING_PTR(rb_scratch_out),
                              (unsigned int)out_capacity);

    RB_GC_GUARD(rb_bytes);
    RB_GC_GUARD(rb_scratch_out);

    if (written == -1) {
        rb_raise(rb_eOutputOverflow,
                 "Hyperion::H2Codec::CGlue.decode_v3 output buffer overflow "
                 "(capacity=%ld)", out_capacity);
    }
    if (written < 0) {
        rb_raise(rb_eRuntimeError,
                 "Hyperion::H2Codec::CGlue.decode_v3 failed (rc=%d)", written);
    }

    rb_str_set_len(rb_scratch_out, (long)written);
    return INT2NUM(written);
}

.encoder_encode_v3(rb_handle_addr, rb_headers, rb_scratch_out) ⇒ Object




168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'ext/hyperion_http/h2_codec_glue.c', line 168

static VALUE rb_cglue_encoder_encode_v3(VALUE self,
                                        VALUE rb_handle_addr,
                                        VALUE rb_headers,
                                        VALUE rb_scratch_out) {
    (void)self;

    if (!cglue_available || !rust_encode_v2) {
        rb_raise(rb_eRuntimeError,
                 "Hyperion::H2Codec::CGlue not installed (call .install(path) first)");
    }

    Check_Type(rb_headers, T_ARRAY);
    Check_Type(rb_scratch_out, T_STRING);

    void *handle = (void *)(intptr_t)NUM2LL(rb_handle_addr);
    long count = RARRAY_LEN(rb_headers);
    if (count == 0) {
        return INT2FIX(0);
    }

    /* Stack buffers — heap fallback for unusually large header sets. */
    uint64_t  stack_argv[HYP_GLUE_STACK_ARGV_CAP * 4];
    uint8_t   stack_blob[HYP_GLUE_STACK_BLOB_CAP];

    uint64_t *argv = stack_argv;
    uint8_t  *blob = stack_blob;
    int       argv_on_heap = 0;
    int       blob_on_heap = 0;
    size_t    argv_cap     = HYP_GLUE_STACK_ARGV_CAP;
    size_t    blob_cap     = HYP_GLUE_STACK_BLOB_CAP;

    if ((size_t)count > argv_cap) {
        argv = (uint64_t *)malloc((size_t)count * 4 * sizeof(uint64_t));
        if (!argv) {
            rb_raise(rb_eNoMemError, "H2Codec::CGlue argv malloc failed");
        }
        argv_cap = (size_t)count;
        argv_on_heap = 1;
    }

    /* First pass: compute total blob size to decide stack vs heap. */
    size_t total_blob = 0;
    for (long i = 0; i < count; i++) {
        VALUE pair = rb_ary_entry(rb_headers, i);
        if (TYPE(pair) != T_ARRAY || RARRAY_LEN(pair) < 2) {
            if (argv_on_heap) free(argv);
            rb_raise(rb_eArgError,
                     "H2Codec::CGlue.encode_v3: each header must be a [name, value] array");
        }
        VALUE name  = rb_ary_entry(pair, 0);
        VALUE value = rb_ary_entry(pair, 1);
        if (TYPE(name) != T_STRING || TYPE(value) != T_STRING) {
            if (argv_on_heap) free(argv);
            rb_raise(rb_eTypeError,
                     "H2Codec::CGlue.encode_v3: header name and value must be Strings");
        }
        total_blob += (size_t)RSTRING_LEN(name);
        total_blob += (size_t)RSTRING_LEN(value);
    }

    if (total_blob > blob_cap) {
        blob = (uint8_t *)malloc(total_blob);
        if (!blob) {
            if (argv_on_heap) free(argv);
            rb_raise(rb_eNoMemError, "H2Codec::CGlue blob malloc failed");
        }
        blob_cap = total_blob;
        blob_on_heap = 1;
    }

    /* Second pass: pack argv quads + concatenate blob. We *do not*
     * call `name.b` / `value.b` here even when the source encoding
     * isn't ASCII_8BIT — HPACK only cares about the byte sequence.
     * `RSTRING_PTR` + `RSTRING_LEN` give us the raw byte view
     * regardless of the Ruby encoding tag, which avoids a per-header
     * String allocation that the v2 Ruby path could not avoid for
     * non-binary inputs. */
    size_t blob_off = 0;
    for (long i = 0; i < count; i++) {
        VALUE pair  = rb_ary_entry(rb_headers, i);
        VALUE name  = rb_ary_entry(pair, 0);
        VALUE value = rb_ary_entry(pair, 1);

        size_t nl = (size_t)RSTRING_LEN(name);
        size_t vl = (size_t)RSTRING_LEN(value);

        size_t base = (size_t)i * 4;
        argv[base + 0] = (uint64_t)blob_off;
        argv[base + 1] = (uint64_t)nl;
        argv[base + 2] = (uint64_t)(blob_off + nl);
        argv[base + 3] = (uint64_t)vl;

        if (nl > 0) {
            memcpy(blob + blob_off, RSTRING_PTR(name), nl);
        }
        blob_off += nl;
        if (vl > 0) {
            memcpy(blob + blob_off, RSTRING_PTR(value), vl);
        }
        blob_off += vl;
    }

    /* Make sure scratch_out has at least `out_capacity` bytes of
     * usable buffer space. Ruby pre-sized it via `String.new(capacity:)`
     * + `<<` to set the length, so RSTRING_LEN reflects the full
     * usable region (we'll truncate to `written` after the FFI call).
     */
    size_t out_capacity = (size_t)RSTRING_LEN(rb_scratch_out);
    /* rb_str_modify ensures the scratch String is mutable, has its own
     * (unshared) backing buffer, and that RSTRING_PTR is valid for
     * out_capacity bytes of writes. Required before we hand its raw
     * pointer to Rust. */
    rb_str_modify(rb_scratch_out);
    unsigned char *out_ptr = (unsigned char *)RSTRING_PTR(rb_scratch_out);

    long long written = rust_encode_v2(handle,
                                       blob, blob_off,
                                       argv, (size_t)count,
                                       out_ptr, out_capacity);

    if (argv_on_heap) free(argv);
    if (blob_on_heap) free(blob);

    /* Keep the headers array alive across the FFI call — RSTRING_PTR
     * pointers we read from `name`/`value` are only valid while their
     * VALUEs are live and unmoved. */
    RB_GC_GUARD(rb_headers);
    RB_GC_GUARD(rb_scratch_out);

    if (written == -1) {
        rb_raise(rb_eOutputOverflow,
                 "Hyperion::H2Codec::CGlue.encode_v3 output buffer overflow "
                 "(capacity=%zu)", out_capacity);
    }
    if (written < 0) {
        rb_raise(rb_eRuntimeError,
                 "Hyperion::H2Codec::CGlue.encode_v3 failed (rc=%lld)", written);
    }

    /* Truncate the scratch String to the bytes-written count. The
     * caller's Ruby wrapper then `byteslice(0, written)`s it — that
     * single byteslice is the only String alloc per encode call. */
    rb_str_set_len(rb_scratch_out, (long)written);
    return LL2NUM(written);
}

.install(rb_path) ⇒ Object




111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'ext/hyperion_http/h2_codec_glue.c', line 111

static VALUE rb_cglue_install(VALUE self, VALUE rb_path) {
    (void)self;
    Check_Type(rb_path, T_STRING);

    if (cglue_available) {
        /* Idempotent — second call is a no-op. */
        return Qtrue;
    }

    const char *path = StringValueCStr(rb_path);
    void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
    if (!h) {
        return Qfalse;
    }

    rust_abi_version_fn abi_fn =
        (rust_abi_version_fn)dlsym(h, "hyperion_h2_codec_abi_version");
    rust_encode_v2_fn enc_fn =
        (rust_encode_v2_fn)dlsym(h, "hyperion_h2_codec_encoder_encode_v2");
    rust_decode_fn dec_fn =
        (rust_decode_fn)dlsym(h, "hyperion_h2_codec_decoder_decode");

    if (!abi_fn || !enc_fn || !dec_fn) {
        dlclose(h);
        return Qfalse;
    }

    /* ABI 1 is the only version currently shipped. If a future Rust
     * crate bumps the ABI, this guard prevents the v3 path from
     * silently dispatching to a mismatched layout. The v2 (Fiddle)
     * path has its own ABI check. */
    if (abi_fn() != 1) {
        dlclose(h);
        return Qfalse;
    }

    rust_dl_handle   = h;
    rust_abi_version = abi_fn;
    rust_encode_v2   = enc_fn;
    rust_decode      = dec_fn;
    cglue_available  = 1;
    return Qtrue;
}