rage-iodine 5.6.0 → 6.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ffa46cab7e0fb7fc7550dd142f42b0850287fee60cc0d72b7f52757f52720f7
4
- data.tar.gz: c51b8ad9e316c21ed06e6803dcfb5af7bf70fc3eee84a0fd179b2730d2572473
3
+ metadata.gz: 6096c74f8f76040521a3d8d977be278e9da518a65e3a5dedd7f715ebcf8c8ae7
4
+ data.tar.gz: 365e0c9732780d5672f3ca2e9b4ca23cfe1a954cb24a2ec58d013e2a59081e1f
5
5
  SHA512:
6
- metadata.gz: fa99ee4e7abb10b55b6f14aeeaef6bf03e85cf8aff9b33a66f7b4f64097c5ef824e0c3de6663caf00f5404b2d08b663debb60c796a0a44490138a96ac92a832c
7
- data.tar.gz: ca2a4ac6f633955b202dccf09661cb7799ae92e16078ef3776c1648e18e2b1ab620f470f4b99b3c75b7d0e8f9b064fad180fd5c9b32ab6ae0ddd60c9d601b58d
6
+ metadata.gz: 8a8fc73f923a749da72f025797613c4036f79e1405e4f8791b8276a8629cdb511d8f9ec40e1b56087f5bc049058351fd7d126372a179a228de4de77aa3a295b9
7
+ data.tar.gz: 19526c5ba722bebb05e18cb4125f7cf46acd8c129f5766709888718fe45f6876065fb9fd34f5c98ec8ccfa86a962418b607e63fbf4c249bbc9419ce6cb1ef053
data/.gitignore CHANGED
@@ -18,3 +18,4 @@
18
18
  .DS_Store
19
19
  spec/examples.txt
20
20
  spec/log
21
+ _ref
data/AGENTS.md ADDED
@@ -0,0 +1,48 @@
1
+ # Intro
2
+
3
+ High-performance C web server used as the runtime for [Rage](https://github.com/rage-rb/rage), a fiber-based Ruby framework.
4
+
5
+ # Main Components
6
+
7
+ - ext/iodine/fio.[ch] - reactor + base library (spinlocks, atomics, linked lists, strings)
8
+ - ext/iodine/fiobj_str.[ch], fio_ary.[ch], fio_data.[ch] - fio data structures
9
+ - ext/iodine/iodine_caller.[ch] - calling Ruby from C
10
+ - ext/iodine/iodine_http.[ch] - HTTP server, `rack.upgrade` handling
11
+ - ext/iodine/iodine_defer.[ch], iodine_pubsub.[ch] - Iodine Ruby API
12
+ - ext/iodine/scheduler.[ch] - Rage fiber scheduler hooks
13
+ - ext/iodine/iodine_worker_pool.[ch] - Ruby 4.0+ thread pool for `blocking_operation_wait`
14
+
15
+ ## `__http_defer__`
16
+
17
+ `[:__http_defer__, fiber]` is a private contract between `Rage::FiberWrapper` and `iodine_http.c`:
18
+
19
+ 1. Rage schedules each request in a fiber. If application code yields for non-blocking I/O, the Rack call returns `[:__http_defer__, fiber]`
20
+ 2. Iodine pauses the HTTP request, and subscribes to the process-local pub/sub channel
21
+ 3. When the fiber finishes, Rage publishes to that channel to resume the request
22
+
23
+ # rack.upgrade (WebSocket/SSE)
24
+
25
+ Iodine supports WebSocket/SSE. The upgrade flow:
26
+
27
+ 1. Rage sets `env['rack.upgrade?']` to `:websocket`/`:sse` and `env['rack.upgrade'] = CallbackHandler`
28
+ 2. Iodine invokes handler callbacks (`on_open`, `on_message`, `on_close`)
29
+
30
+ See ext/iodine/iodine_http.c and ext/iodine/iodine_connection.c for implementation.
31
+
32
+ # Threading
33
+
34
+ Rage runs Iodine single-threaded; parallelization uses forked processes. New code can exploit this for simplicity, but should remain thread-safe to respect existing Iodine patterns.
35
+
36
+ # Rules
37
+
38
+ ## 1. Think First
39
+ State assumptions. Surface tradeoffs. If unclear, ask - don't guess.
40
+
41
+ ## 2. Simplicity
42
+ Minimum code for the problem. No speculative features, no premature abstractions, no handling impossible cases. If 200 lines could be 50, rewrite.
43
+
44
+ ## 3. Surgical Changes
45
+ Touch only what's needed. Match existing style. Remove only orphans YOUR changes created. Every changed line should trace to the request.
46
+
47
+ ## 4. Verify
48
+ Define success criteria upfront. Transform vague asks into testable goals ("fix bug" → "write failing test, make it pass").
data/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ Please notice that this change log contains changes for upcoming releases as wel
6
6
 
7
7
  ## Changes:
8
8
 
9
+ #### Change log v.6.1.0 (2026-10-04)
10
+
11
+ **Update**: Refactor parameters parsing
12
+
13
+ #### Change log v.6.0.0 (2026-09-06)
14
+
15
+ **Update**: Update scheduler `read`/`write` hooks
16
+
9
17
  #### Change log v.5.6.0 (2026-09-06)
10
18
 
11
19
  **Update**: TCP Socket Listen: Accept Queue
data/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
data/ext/iodine/fio.c CHANGED
@@ -3687,12 +3687,12 @@ retry_int:
3687
3687
  }
3688
3688
 
3689
3689
  /**
3690
- * This is a complete copy of `fio_read` except that it doesn't attempt to force close the
3691
- * socker on error.
3690
+ * Performs a single read operation using the connection's read hook.
3692
3691
  *
3693
- * The method is currently used by the fiber scheduler.
3692
+ * The return value and errno match the underlying read operation. The socket
3693
+ * isn't closed on error.
3694
3694
  */
3695
- ssize_t fio_read_unsafe(intptr_t uuid, void *buffer, size_t count) {
3695
+ ssize_t fio_read_once(intptr_t uuid, void *buffer, size_t count) {
3696
3696
  if (!uuid_is_valid(uuid) || !uuid_data(uuid).open) {
3697
3697
  errno = EBADF;
3698
3698
  return -1;
@@ -3704,22 +3704,36 @@ ssize_t fio_read_unsafe(intptr_t uuid, void *buffer, size_t count) {
3704
3704
  uuid_data(uuid).rw_hooks->read;
3705
3705
  void *udata = uuid_data(uuid).rw_udata;
3706
3706
  fio_unlock(&uuid_data(uuid).sock_lock);
3707
- int old_errno = errno;
3708
- ssize_t ret;
3709
- retry_int:
3710
- ret = rw_read(uuid, udata, buffer, count);
3707
+ ssize_t ret = rw_read(uuid, udata, buffer, count);
3711
3708
  if (ret > 0) {
3712
3709
  fio_touch(uuid);
3713
- return ret;
3714
3710
  }
3715
- if (ret < 0 && errno == EINTR)
3716
- goto retry_int;
3717
- if (ret < 0 &&
3718
- (errno == EWOULDBLOCK || errno == EAGAIN || errno == ENOTCONN)) {
3719
- errno = old_errno;
3711
+ return ret;
3712
+ }
3713
+
3714
+ /**
3715
+ * Performs a single write operation using the connection's write hook.
3716
+ *
3717
+ * The return value and errno match the underlying write operation. The socket
3718
+ * isn't closed on error and no data is queued for the reactor.
3719
+ */
3720
+ ssize_t fio_write_once(intptr_t uuid, const void *buffer, size_t count) {
3721
+ if (!uuid_is_valid(uuid) || !uuid_data(uuid).open) {
3722
+ errno = EBADF;
3723
+ return -1;
3724
+ }
3725
+ if (count == 0)
3720
3726
  return 0;
3727
+ fio_lock(&uuid_data(uuid).sock_lock);
3728
+ ssize_t (*rw_write)(intptr_t, void *, const void *, size_t) =
3729
+ uuid_data(uuid).rw_hooks->write;
3730
+ void *udata = uuid_data(uuid).rw_udata;
3731
+ fio_unlock(&uuid_data(uuid).sock_lock);
3732
+ ssize_t ret = rw_write(uuid, udata, buffer, count);
3733
+ if (ret > 0) {
3734
+ fio_touch(uuid);
3721
3735
  }
3722
- return -1;
3736
+ return ret;
3723
3737
  }
3724
3738
 
3725
3739
  /**
data/ext/iodine/fio.h CHANGED
@@ -1198,12 +1198,20 @@ size_t fio_local_addr(char *dest, size_t limit);
1198
1198
  ssize_t fio_read(intptr_t uuid, void *buffer, size_t count);
1199
1199
 
1200
1200
  /**
1201
- * This is a complete copy of `fio_read` except that it doesn't attempt to force close the
1202
- * socker on error.
1201
+ * Performs a single read operation using the connection's read hook.
1203
1202
  *
1204
- * The method is currently used by the fiber scheduler.
1203
+ * The return value and errno match the underlying read operation. The socket
1204
+ * isn't closed on error.
1205
1205
  */
1206
- ssize_t fio_read_unsafe(intptr_t uuid, void *buffer, size_t count);
1206
+ ssize_t fio_read_once(intptr_t uuid, void *buffer, size_t count);
1207
+
1208
+ /**
1209
+ * Performs a single write operation using the connection's write hook.
1210
+ *
1211
+ * The return value and errno match the underlying write operation. The socket
1212
+ * isn't closed on error and no data is queued for the reactor.
1213
+ */
1214
+ ssize_t fio_write_once(intptr_t uuid, const void *buffer, size_t count);
1207
1215
 
1208
1216
  /** The following structure is used for `fio_write2_fn` function arguments. */
1209
1217
  typedef struct {
data/ext/iodine/http.c CHANGED
@@ -1842,8 +1842,8 @@ typedef struct {
1842
1842
 
1843
1843
  #define http_mime_parser2fio(parser) ((http_fio_mime_s *)(parser))
1844
1844
 
1845
- /** Parse a parameter key and add it to the `params` hash. Check `parse_nested_query_internal` for reference. */
1846
- static void add_to_params(VALUE params, char *key, size_t key_len, VALUE value) {
1845
+ /** Parse a decoded parameter key and add it to the `params` hash. */
1846
+ void http_add_to_params(VALUE params, char *key, size_t key_len, VALUE value) {
1847
1847
  char *pos = NULL;
1848
1848
  if (key_len > 1) {
1849
1849
  pos = memchr(key + 1, '[', key_len - 1);
@@ -1925,7 +1925,7 @@ static void add_to_params(VALUE params, char *key, size_t key_len, VALUE value)
1925
1925
  if (RB_TYPE_P(nested_val, T_HASH)) {
1926
1926
  char *n_pos_start = k_pos + 1, *n_pos_end = k_pos + 1;
1927
1927
 
1928
- while (nested_val != Qnil && *n_pos_start == '[' && *(n_pos_start + 1) != ']' && *n_pos_end != '&' && *n_pos_end != '=' && n_pos_end < end) {
1928
+ while (nested_val != Qnil && n_pos_end < end && *n_pos_start == '[' && *(n_pos_start + 1) != ']') {
1929
1929
  n_pos_end++;
1930
1930
 
1931
1931
  if (*n_pos_end == ']') {
@@ -2011,7 +2011,7 @@ static void http_mime_parser_on_data(http_mime_parser_s *parser, void *name,
2011
2011
  // for regular values - just add them to params
2012
2012
  if (!filename_len) {
2013
2013
  VALUE r_value = rb_enc_str_new(value, value_len, IodineBinaryEncoding);
2014
- add_to_params(http_mime_parser2fio(parser)->params, name, name_len, r_value);
2014
+ http_add_to_params(http_mime_parser2fio(parser)->params, name, name_len, r_value);
2015
2015
  return;
2016
2016
  }
2017
2017
 
@@ -2024,7 +2024,7 @@ static void http_mime_parser_on_data(http_mime_parser_s *parser, void *name,
2024
2024
  close(file);
2025
2025
 
2026
2026
  VALUE r_value = build_file_value(r_tempfile, filename, filename_len, mimetype, mimetype_len);
2027
- add_to_params(http_mime_parser2fio(parser)->params, name, name_len, r_value);
2027
+ http_add_to_params(http_mime_parser2fio(parser)->params, name, name_len, r_value);
2028
2028
  }
2029
2029
 
2030
2030
  /** Called when the data didn't fit in the buffer. Data will be streamed. */
@@ -2073,7 +2073,7 @@ static void http_mime_parser_on_partial_end(http_mime_parser_s *parser) {
2073
2073
 
2074
2074
  // for a file - add to params and close the fd
2075
2075
  if (fio_parser->partial_fd) {
2076
- add_to_params(fio_parser->params, RSTRING_PTR(fio_parser->partial_name), RSTRING_LEN(fio_parser->partial_name), fio_parser->partial_tempfile);
2076
+ http_add_to_params(fio_parser->params, RSTRING_PTR(fio_parser->partial_name), RSTRING_LEN(fio_parser->partial_name), fio_parser->partial_tempfile);
2077
2077
  close(fio_parser->partial_fd);
2078
2078
 
2079
2079
  fio_parser->partial_name = Qnil;
@@ -2086,7 +2086,7 @@ static void http_mime_parser_on_partial_end(http_mime_parser_s *parser) {
2086
2086
  FIOBJ slice = fiobj_data_slice(fio_parser->h->body, fio_parser->partial_offset, fio_parser->partial_length);
2087
2087
  fio_str_info_s s_slice = fiobj_obj2cstr(slice);
2088
2088
  VALUE r_value = rb_enc_str_new(s_slice.data, s_slice.len, IodineBinaryEncoding);
2089
- add_to_params(fio_parser->params, RSTRING_PTR(fio_parser->partial_name), RSTRING_LEN(fio_parser->partial_name), r_value);
2089
+ http_add_to_params(fio_parser->params, RSTRING_PTR(fio_parser->partial_name), RSTRING_LEN(fio_parser->partial_name), r_value);
2090
2090
 
2091
2091
  fio_parser->partial_name = Qnil;
2092
2092
  fio_parser->partial_length = 0;
data/ext/iodine/http.h CHANGED
@@ -781,6 +781,9 @@ HTTP GET and POST parsing helpers
781
781
  */
782
782
  VALUE http_parse_multipart(http_s *h, char *content_type, size_t content_type_len);
783
783
 
784
+ /** Adds a decoded parameter key and Ruby value to a nested parameters hash. */
785
+ void http_add_to_params(VALUE params, char *key, size_t key_len, VALUE value);
786
+
784
787
  /**
785
788
  * Parses the query part of an HTTP request/response. Uses `http_add2hash`.
786
789
  *
@@ -210,7 +210,7 @@ static VALUE iodine_rfc2109(VALUE self, VALUE rtm) {
210
210
  }
211
211
 
212
212
  /**
213
- Convert query string into a Ruby object in (almost always) one pass with no recursion.
213
+ Convert query string into a Ruby object without recursion.
214
214
 
215
215
 
216
216
  Iodine::Rack::Utils.parse_nested_query("a=1&b[]=2&c[d]=3") # => { "a" => "1", "b" => ["2"], "c" => { "d" => "3" } }
@@ -240,182 +240,68 @@ Convert query string into a Ruby object in (almost always) one pass with no recu
240
240
  Rack: 64257.9 i/s - 16.94x slower
241
241
 
242
242
  */
243
+ static VALUE build_query_component(char *str, size_t len,
244
+ uint8_t should_decode) {
245
+ VALUE result = rb_str_new(str, len);
246
+ if (should_decode) {
247
+ url_decode_inplace(Qnil, result);
248
+ }
249
+ return result;
250
+ }
251
+
243
252
  static VALUE parse_nested_query_internal(char *str, size_t len) {
244
- uint8_t should_decode = 0;
245
253
  VALUE params = rb_hash_new();
246
-
247
- char *pos = str, *k_pos = str, *v_pos = str;
248
- const char *end = str + len;
254
+ char *pos = str;
255
+ char *end = str + len;
249
256
 
250
257
  while (pos < end) {
251
- k_pos++;
252
-
253
- if (*k_pos == '=') { // plain param
254
- v_pos = k_pos + 1;
255
- while (*v_pos != '&' && v_pos < end) {
256
- if (*v_pos == '%' || *v_pos == '+') {
257
- should_decode = 1;
258
- }
259
- v_pos++;
260
- }
261
-
262
- VALUE k = ID2SYM(rb_intern2(pos, k_pos - pos)), v = rb_str_new(k_pos + 1, v_pos - k_pos - 1);
263
- if (should_decode) {
264
- rb_hash_aset(params, k, url_decode_inplace(Qnil, v));
265
- should_decode = 0;
266
- } else {
267
- rb_hash_aset(params, k, v);
268
- }
269
- pos = k_pos = v_pos + 1;
270
-
271
- } else if (*k_pos == '[') { // things are about to get rough now
272
- VALUE arr = Qnil, hsh = Qnil, hsh_key = Qnil;
273
- VALUE k = ID2SYM(rb_intern2(pos, k_pos - pos)), v;
274
- pos = k_pos;
275
- uint8_t depth = 0;
276
-
277
- while (*pos != '=' && pos < end) {
278
- if (depth++ == PARAMS_MAX_DEPTH) {
279
- rb_raise(rb_eRuntimeError, "Params too deep");
280
- }
281
-
282
- if (*pos == '[' && *(pos + 1) == ']') { // array
283
- if (arr != Qnil) {
284
- VALUE tmp = rb_ary_new();
285
- rb_ary_push(arr, tmp);
286
- arr = tmp;
287
- } else if (hsh != Qnil) {
288
- arr = rb_hash_aref(hsh, hsh_key);
289
- if (arr == Qnil) {
290
- arr = rb_ary_new();
291
- rb_hash_aset(hsh, hsh_key, arr);
292
- } else {
293
- Check_Type(arr, T_ARRAY);
294
- }
295
- hsh = hsh_key = Qnil;
296
- } else {
297
- VALUE tmp = rb_hash_aref(params, k);
298
- if (tmp != Qnil) {
299
- Check_Type(tmp, T_ARRAY);
300
- arr = tmp;
301
- } else {
302
- arr = rb_ary_new();
303
- rb_hash_aset(params, k, arr);
304
- }
305
- }
306
-
307
- pos += 2;
308
-
309
- } else if (*pos == '[' && *(pos + 1) != ']') { // hash
310
- if (pos + 2 < end) {
311
- k_pos = memchr(pos + 2, ']', end - pos - 2);
312
- } else {
313
- k_pos = NULL;
314
- }
315
-
316
- if (!k_pos) {
317
- rb_raise(rb_eRuntimeError, "Bad params");
318
- }
319
-
320
- VALUE prev_hsh_key = hsh_key;
321
- hsh_key = ID2SYM(rb_intern2(pos + 1, k_pos - pos - 1));
322
-
323
- if (hsh != Qnil) {
324
- VALUE existing = rb_hash_aref(hsh, prev_hsh_key);
325
- if (existing != Qnil) {
326
- Check_Type(existing, T_HASH);
327
- hsh = existing;
328
- } else {
329
- VALUE tmp = rb_hash_new();
330
- rb_hash_aset(hsh, prev_hsh_key, tmp);
331
- hsh = tmp;
332
- }
333
- } else if (arr != Qnil) {
334
- VALUE nested_val = Qnil;
335
- if (RARRAY_LEN(arr) != 0) {
336
- VALUE tmp = rb_ary_entry(arr, -1);
337
- Check_Type(tmp, T_HASH);
338
- nested_val = rb_hash_aref(tmp, hsh_key);
339
- }
340
-
341
- // handle Rails' smart key grouping;
342
- // "users[][data][id]=11&users[][data][name]=ross&users[][data][id]=22&users[][data][name]=chandler"
343
- // should result in:
344
- // { users: [{ data: { id: "11", name: "ross" } }, { data: { id: "22", name: "chandler" } }] }
345
- if (RB_TYPE_P(nested_val, T_HASH)) {
346
- char *n_pos_start = k_pos + 1, *n_pos_end = k_pos + 1;
347
-
348
- while (nested_val != Qnil && *n_pos_start == '[' && *(n_pos_start + 1) != ']' && *n_pos_end != '&' && *n_pos_end != '=' && n_pos_end < end) {
349
- n_pos_end++;
350
-
351
- if (*n_pos_end == ']') {
352
- VALUE nested_key = ID2SYM(rb_intern2(n_pos_start + 1, n_pos_end - n_pos_start - 1));
353
- Check_Type(nested_val, T_HASH);
354
- nested_val = rb_hash_aref(nested_val, nested_key);
355
- n_pos_start = ++n_pos_end;
356
- }
357
- }
358
- }
359
-
360
- // use a new hash if there's none under this key or the existing one already has such key
361
- if (RARRAY_LEN(arr) == 0 || nested_val != Qnil) {
362
- hsh = rb_hash_new();
363
- rb_ary_push(arr, hsh);
364
- } else {
365
- hsh = rb_ary_entry(arr, -1);
366
- if (hsh != Qnil) {
367
- Check_Type(hsh, T_HASH);
368
- }
369
- }
370
-
371
- arr = Qnil;
372
- } else {
373
- VALUE tmp = rb_hash_aref(params, k);
374
- if (tmp != Qnil) {
375
- Check_Type(tmp, T_HASH);
376
- hsh = tmp;
377
- } else {
378
- hsh = rb_hash_new();
379
- rb_hash_aset(params, k, hsh);
380
- }
381
- }
382
-
383
- pos = k_pos + 1;
384
-
258
+ char *pair_end = pos;
259
+ char *separator = NULL;
260
+ uint8_t key_should_decode = 0;
261
+ uint8_t value_should_decode = 0;
262
+
263
+ while (pair_end < end && *pair_end != '&') {
264
+ if (!separator && *pair_end == '=') {
265
+ separator = pair_end;
266
+ } else if (*pair_end == '%' || *pair_end == '+') {
267
+ if (separator) {
268
+ value_should_decode = 1;
385
269
  } else {
386
- pos++;
270
+ key_should_decode = 1;
387
271
  }
388
272
  }
273
+ pair_end++;
274
+ }
389
275
 
390
- //
391
- // write the final object into `params`
392
- //
393
-
394
- for (v_pos = pos + 1; *v_pos != '&' && v_pos < end; v_pos++) {
395
- if (*v_pos == '%' || *v_pos == '+') {
396
- should_decode = 1;
397
- }
398
- }
276
+ char *key_end = separator ? separator : pair_end;
277
+ VALUE key = Qnil;
278
+ char *key_data = pos;
279
+ size_t key_len = key_end - pos;
399
280
 
400
- v = rb_str_new(pos + 1, v_pos - pos - 1);
401
- if (should_decode) {
402
- url_decode_inplace(Qnil, v);
403
- should_decode = 0;
404
- }
281
+ if (key_should_decode) {
282
+ key = build_query_component(pos, key_len, 1);
283
+ key_data = RSTRING_PTR(key);
284
+ key_len = RSTRING_LEN(key);
285
+ }
405
286
 
406
- if (arr != Qnil) {
407
- rb_ary_push(arr, v);
408
- } else {
409
- rb_hash_aset(hsh, hsh_key, v);
410
- }
287
+ VALUE value;
288
+ if (separator) {
289
+ value = build_query_component(separator + 1,
290
+ pair_end - separator - 1,
291
+ value_should_decode);
292
+ } else if (key_len > 1 && memchr(key_data + 1, '[', key_len - 1)) {
293
+ value = rb_str_new(NULL, 0);
294
+ } else {
295
+ value = Qnil;
296
+ }
411
297
 
412
- pos = k_pos = v_pos + 1;
298
+ http_add_to_params(params, key_data, key_len, value);
299
+ RB_GC_GUARD(key);
413
300
 
414
- } else if (*k_pos == '&' || k_pos >= end) {
415
- VALUE k = ID2SYM(rb_intern2(pos, k_pos - pos));
416
- rb_hash_aset(params, k, Qnil);
417
- pos = k_pos + 1;
301
+ if (pair_end == end) {
302
+ break;
418
303
  }
304
+ pos = pair_end + 1;
419
305
  }
420
306
 
421
307
  return params;
@@ -425,21 +311,26 @@ static VALUE parse_nested_query_internal(char *str, size_t len) {
425
311
  Convert query string into a Ruby object.
426
312
  */
427
313
  static VALUE parse_nested_query(VALUE self, VALUE r_str) {
428
- return parse_nested_query_internal(RSTRING_PTR(r_str), RSTRING_LEN(r_str));
429
314
  (void)self;
315
+ Check_Type(r_str, T_STRING);
316
+ VALUE result =
317
+ parse_nested_query_internal(RSTRING_PTR(r_str), RSTRING_LEN(r_str));
318
+ RB_GC_GUARD(r_str);
319
+ return result;
430
320
  }
431
321
 
432
322
  /**
433
323
  Convert urlencoded body into a Ruby object.
434
324
  */
435
325
  static VALUE parse_urlencoded_nested_query(VALUE self, VALUE r_str) {
436
- ssize_t len = http_decode_url(RSTRING_PTR(r_str), RSTRING_PTR(r_str), RSTRING_LEN(r_str));
437
- if (len < 0) {
438
- rb_raise(rb_eRuntimeError, "Invalid encoding");
439
- }
440
-
441
- return parse_nested_query_internal(RSTRING_PTR(r_str), len);
442
326
  (void)self;
327
+ Check_Type(r_str, T_STRING);
328
+ char *str = RSTRING_PTR(r_str);
329
+ size_t len = RSTRING_LEN(r_str);
330
+
331
+ VALUE result = parse_nested_query_internal(str, len);
332
+ RB_GC_GUARD(r_str);
333
+ return result;
443
334
  }
444
335
 
445
336
  /**
@@ -1,6 +1,8 @@
1
1
  #include "iodine.h"
2
2
  #include "iodine_store.h"
3
3
  #include "ruby.h"
4
+ #include "ruby/fiber/scheduler.h"
5
+ #include "ruby/io/buffer.h"
4
6
 
5
7
  #include <errno.h>
6
8
  #include <stddef.h>
@@ -10,7 +12,7 @@
10
12
 
11
13
  #include "fio.h"
12
14
 
13
- #define IO_MAX_READ 8192
15
+ #define IO_MAX_READ 65536
14
16
 
15
17
  static ID call_id;
16
18
  static uint8_t ATTACH_ON_READ_READY_CALLBACK;
@@ -151,51 +153,104 @@ static VALUE iodine_scheduler_attach(VALUE self, VALUE r_fd, VALUE r_waittype, V
151
153
  (void)self;
152
154
  }
153
155
 
154
- static VALUE iodine_scheduler_write(VALUE self, VALUE r_fd, VALUE r_buffer, VALUE r_length, VALUE r_offset) {
156
+ static VALUE iodine_scheduler_write_async(VALUE self, VALUE r_fd, VALUE r_buffer, VALUE r_length, VALUE r_offset) {
155
157
  Check_Type(r_fd, T_FIXNUM);
156
158
  int fd = FIX2INT(r_fd);
157
159
 
158
- Check_Type(r_buffer, T_STRING);
159
- char *buffer = RSTRING_PTR(r_buffer);
160
+ const void *buffer;
161
+ size_t buffer_length;
162
+ rb_io_buffer_get_bytes_for_reading(r_buffer, &buffer, &buffer_length);
160
163
 
161
164
  Check_Type(r_length, T_FIXNUM);
162
- int length = FIX2INT(r_length);
165
+ size_t length = NUM2SIZET(r_length);
163
166
 
164
167
  Check_Type(r_offset, T_FIXNUM);
165
- int offset = FIX2INT(r_offset);
168
+ size_t offset = NUM2SIZET(r_offset);
169
+
170
+ if (offset > buffer_length || length > buffer_length - offset) {
171
+ return rb_fiber_scheduler_io_result(-1, EINVAL);
172
+ }
173
+ if (!length) {
174
+ return r_length;
175
+ }
166
176
 
167
177
  void *cpy = fio_malloc(length);
168
- memcpy(cpy, buffer, length);
169
- fio_write2(fio_fd2uuid(fd), .data.buffer = cpy, .length = length, .offset = offset, .after.dealloc = fio_free);
178
+ memcpy(cpy, (const char *)buffer + offset, length);
179
+ fio_write2(fio_fd2uuid(fd), .data.buffer = cpy, .length = length, .after.dealloc = fio_free);
170
180
 
171
181
  return r_length;
172
182
 
173
183
  (void)self;
174
184
  }
175
185
 
176
- static VALUE iodine_scheduler_read(VALUE self, VALUE r_fd, VALUE r_length, VALUE r_offset) {
186
+ static VALUE iodine_scheduler_write(VALUE self, VALUE r_fd, VALUE r_buffer, VALUE r_length, VALUE r_offset) {
177
187
  Check_Type(r_fd, T_FIXNUM);
178
188
  int fd = FIX2INT(r_fd);
179
189
 
190
+ const void *buffer;
191
+ size_t buffer_length;
192
+ rb_io_buffer_get_bytes_for_reading(r_buffer, &buffer, &buffer_length);
193
+
180
194
  Check_Type(r_length, T_FIXNUM);
181
- int length = FIX2INT(r_length);
195
+ size_t length = NUM2SIZET(r_length);
182
196
 
183
- if (length == 0) {
184
- length = IO_MAX_READ;
197
+ Check_Type(r_offset, T_FIXNUM);
198
+ size_t offset = NUM2SIZET(r_offset);
199
+
200
+ if (offset > buffer_length || length > buffer_length - offset) {
201
+ return rb_fiber_scheduler_io_result(-1, EINVAL);
202
+ }
203
+ if (!length) {
204
+ return rb_fiber_scheduler_io_result(0, 0);
185
205
  }
186
206
 
187
- intptr_t uuid = fio_fd2uuid(fd);
188
- char buffer[length];
207
+ ssize_t result = fio_write_once(fio_fd2uuid(fd), (const char *)buffer + offset, length);
208
+ int error = result < 0 ? errno : 0;
209
+ return rb_fiber_scheduler_io_result(result, error);
210
+
211
+ (void)self;
212
+ }
213
+
214
+ static VALUE iodine_scheduler_read(VALUE self, VALUE r_fd, VALUE r_buffer, VALUE r_length, VALUE r_offset) {
215
+ Check_Type(r_fd, T_FIXNUM);
216
+ int fd = FIX2INT(r_fd);
217
+
218
+ void *buffer;
219
+ size_t buffer_length;
220
+ rb_io_buffer_get_bytes_for_writing(r_buffer, &buffer, &buffer_length);
221
+
222
+ Check_Type(r_length, T_FIXNUM);
223
+ size_t length = NUM2SIZET(r_length);
224
+
225
+ Check_Type(r_offset, T_FIXNUM);
226
+ size_t offset = NUM2SIZET(r_offset);
227
+
228
+ if (offset > buffer_length || length > buffer_length - offset) {
229
+ return rb_fiber_scheduler_io_result(-1, EINVAL);
230
+ }
189
231
 
190
- ssize_t len = fio_read_unsafe(uuid, &buffer, length);
191
- if (len == -1) {
192
- return Qnil;
232
+ #if RUBY_FIBER_SCHEDULER_VERSION >= 4
233
+ if (length > IO_MAX_READ) {
234
+ length = IO_MAX_READ;
235
+ }
236
+ #else
237
+ if (length == 0) {
238
+ length = buffer_length - offset;
239
+ if (length > IO_MAX_READ) {
240
+ length = IO_MAX_READ;
241
+ }
242
+ }
243
+ #endif
244
+
245
+ if (!length) {
246
+ return rb_fiber_scheduler_io_result(0, 0);
193
247
  }
194
248
 
195
- return rb_str_new(buffer, len);
249
+ ssize_t result = fio_read_once(fio_fd2uuid(fd), (char *)buffer + offset, length);
250
+ int error = result < 0 ? errno : 0;
251
+ return rb_fiber_scheduler_io_result(result, error);
196
252
 
197
253
  (void)self;
198
- (void)r_offset;
199
254
  }
200
255
 
201
256
  static VALUE iodine_scheduler_close(VALUE self) {
@@ -219,8 +274,9 @@ void iodine_scheduler_initialize(void) {
219
274
  VALUE SchedulerModule = rb_define_module_under(IodineModule, "Scheduler");
220
275
 
221
276
  rb_define_module_function(SchedulerModule, "attach", iodine_scheduler_attach, 3);
277
+ rb_define_module_function(SchedulerModule, "write_async", iodine_scheduler_write_async, 4);
222
278
  rb_define_module_function(SchedulerModule, "write", iodine_scheduler_write, 4);
223
- rb_define_module_function(SchedulerModule, "read", iodine_scheduler_read, 3);
279
+ rb_define_module_function(SchedulerModule, "read", iodine_scheduler_read, 4);
224
280
  rb_define_module_function(SchedulerModule, "close", iodine_scheduler_close, 0);
225
281
 
226
282
  VALUE cIO = rb_const_get(rb_cObject, rb_intern2("IO", 2));
@@ -1,3 +1,3 @@
1
1
  module Iodine
2
- VERSION = '5.6.0'.freeze
2
+ VERSION = '6.1.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage-iodine
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boaz Segev
@@ -125,7 +125,9 @@ files:
125
125
  - ".gitignore"
126
126
  - ".rspec"
127
127
  - ".yardopts"
128
+ - AGENTS.md
128
129
  - CHANGELOG.md
130
+ - CLAUDE.md
129
131
  - Gemfile
130
132
  - LICENSE.txt
131
133
  - LIMITS.md