iodine 0.7.59 → 0.8.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.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/.clang-format +15 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -3
  5. data/CHANGELOG.md +132 -12
  6. data/Gemfile +5 -7
  7. data/LICENSE.txt +1 -1
  8. data/README.md +147 -671
  9. data/Rakefile +9 -16
  10. data/examples/chatroom.nru +9 -0
  11. data/examples/client +74 -0
  12. data/examples/config-hello.nru +9 -0
  13. data/examples/config-hello.ru +11 -0
  14. data/examples/config-hijack.ru +13 -0
  15. data/examples/config-shootout.ru +43 -0
  16. data/examples/config.nru +120 -0
  17. data/examples/config.rb +37 -0
  18. data/examples/config.ru +42 -53
  19. data/examples/fake +24 -0
  20. data/examples/hello_neorack +137 -0
  21. data/examples/routes.nru +29 -0
  22. data/examples/routes.ru +42 -0
  23. data/exe/iodine +218 -258
  24. data/ext/iodine/extconf.rb +27 -100
  25. data/ext/iodine/fio-stl.h +128336 -0
  26. data/ext/iodine/fio-stl.md +26396 -0
  27. data/ext/iodine/iodine.c +213 -1393
  28. data/ext/iodine/iodine.h +250 -55
  29. data/ext/iodine/iodine_arg_helper.h +329 -0
  30. data/ext/iodine/iodine_caller.h +234 -25
  31. data/ext/iodine/iodine_cli.h +434 -0
  32. data/ext/iodine/iodine_compression.h +312 -0
  33. data/ext/iodine/iodine_connection.h +3484 -34
  34. data/ext/iodine/iodine_core.h +363 -0
  35. data/ext/iodine/iodine_crypto.h +1441 -0
  36. data/ext/iodine/iodine_defer.h +225 -4
  37. data/ext/iodine/iodine_json.h +738 -4
  38. data/ext/iodine/iodine_listener.h +263 -0
  39. data/ext/iodine/iodine_log.h +297 -0
  40. data/ext/iodine/iodine_minimap.h +543 -0
  41. data/ext/iodine/iodine_mustache.h +461 -4
  42. data/ext/iodine/iodine_pubsub_eng.h +729 -0
  43. data/ext/iodine/iodine_pubsub_history.h +547 -0
  44. data/ext/iodine/iodine_pubsub_msg.h +228 -0
  45. data/ext/iodine/iodine_redis.h +322 -0
  46. data/ext/iodine/iodine_store.h +418 -16
  47. data/ext/iodine/iodine_threads.h +279 -0
  48. data/ext/iodine/iodine_tls.h +515 -6
  49. data/ext/iodine/iodine_utils.h +923 -0
  50. data/iodine.gemspec +37 -42
  51. data/lib/iodine/benchmark.rb +1690 -0
  52. data/lib/iodine/documentation.rb +3085 -0
  53. data/lib/iodine/version.rb +17 -1
  54. data/lib/iodine.rb +54 -255
  55. data/logo.svg +36 -0
  56. data/mustache.md +55 -0
  57. data/sig/iodine.rbs +4 -0
  58. metadata +58 -184
  59. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  60. data/.github/workflows/ruby.yml +0 -42
  61. data/.gitignore +0 -20
  62. data/LIMITS.md +0 -41
  63. data/SPEC-PubSub-Draft.md +0 -159
  64. data/SPEC-WebSocket-Draft.md +0 -239
  65. data/bin/console +0 -22
  66. data/bin/info.md +0 -353
  67. data/bin/mustache_bench.rb +0 -100
  68. data/bin/poc/Gemfile.lock +0 -23
  69. data/bin/poc/README.md +0 -37
  70. data/bin/poc/config.ru +0 -66
  71. data/bin/poc/gemfile +0 -1
  72. data/bin/poc/www/index.html +0 -57
  73. data/examples/async_task.ru +0 -92
  74. data/examples/bates/README.md +0 -3
  75. data/examples/bates/config.ru +0 -342
  76. data/examples/bates/david+bold.pdf +0 -0
  77. data/examples/bates/public/drop-pdf.png +0 -0
  78. data/examples/bates/public/index.html +0 -600
  79. data/examples/echo.ru +0 -59
  80. data/examples/etag.ru +0 -16
  81. data/examples/hello.ru +0 -29
  82. data/examples/pubsub_engine.ru +0 -81
  83. data/examples/rack3.ru +0 -12
  84. data/examples/redis.ru +0 -70
  85. data/examples/shootout.ru +0 -73
  86. data/examples/sub-protocols.ru +0 -90
  87. data/examples/tcp_client.rb +0 -66
  88. data/examples/x-sendfile.ru +0 -14
  89. data/ext/iodine/fio.c +0 -11995
  90. data/ext/iodine/fio.h +0 -6371
  91. data/ext/iodine/fio_cli.c +0 -431
  92. data/ext/iodine/fio_cli.h +0 -189
  93. data/ext/iodine/fio_json_parser.h +0 -688
  94. data/ext/iodine/fio_siphash.c +0 -157
  95. data/ext/iodine/fio_siphash.h +0 -37
  96. data/ext/iodine/fio_tls.h +0 -129
  97. data/ext/iodine/fio_tls_missing.c +0 -649
  98. data/ext/iodine/fio_tls_openssl.c +0 -1106
  99. data/ext/iodine/fio_tmpfile.h +0 -50
  100. data/ext/iodine/fiobj.h +0 -44
  101. data/ext/iodine/fiobj4fio.h +0 -21
  102. data/ext/iodine/fiobj_ary.c +0 -333
  103. data/ext/iodine/fiobj_ary.h +0 -139
  104. data/ext/iodine/fiobj_data.c +0 -1187
  105. data/ext/iodine/fiobj_data.h +0 -167
  106. data/ext/iodine/fiobj_hash.c +0 -409
  107. data/ext/iodine/fiobj_hash.h +0 -176
  108. data/ext/iodine/fiobj_json.c +0 -622
  109. data/ext/iodine/fiobj_json.h +0 -68
  110. data/ext/iodine/fiobj_mem.h +0 -71
  111. data/ext/iodine/fiobj_mustache.c +0 -317
  112. data/ext/iodine/fiobj_mustache.h +0 -62
  113. data/ext/iodine/fiobj_numbers.c +0 -344
  114. data/ext/iodine/fiobj_numbers.h +0 -127
  115. data/ext/iodine/fiobj_str.c +0 -433
  116. data/ext/iodine/fiobj_str.h +0 -172
  117. data/ext/iodine/fiobject.c +0 -620
  118. data/ext/iodine/fiobject.h +0 -655
  119. data/ext/iodine/hpack.h +0 -1923
  120. data/ext/iodine/http.c +0 -2743
  121. data/ext/iodine/http.h +0 -1002
  122. data/ext/iodine/http1.c +0 -910
  123. data/ext/iodine/http1.h +0 -29
  124. data/ext/iodine/http1_parser.h +0 -878
  125. data/ext/iodine/http_internal.c +0 -1279
  126. data/ext/iodine/http_internal.h +0 -237
  127. data/ext/iodine/http_mime_parser.h +0 -350
  128. data/ext/iodine/iodine_caller.c +0 -230
  129. data/ext/iodine/iodine_connection.c +0 -941
  130. data/ext/iodine/iodine_defer.c +0 -420
  131. data/ext/iodine/iodine_fiobj2rb.h +0 -120
  132. data/ext/iodine/iodine_helpers.c +0 -282
  133. data/ext/iodine/iodine_helpers.h +0 -12
  134. data/ext/iodine/iodine_http.c +0 -1181
  135. data/ext/iodine/iodine_http.h +0 -23
  136. data/ext/iodine/iodine_json.c +0 -302
  137. data/ext/iodine/iodine_mustache.c +0 -567
  138. data/ext/iodine/iodine_pubsub.c +0 -580
  139. data/ext/iodine/iodine_pubsub.h +0 -26
  140. data/ext/iodine/iodine_rack_io.c +0 -273
  141. data/ext/iodine/iodine_rack_io.h +0 -20
  142. data/ext/iodine/iodine_store.c +0 -146
  143. data/ext/iodine/iodine_tcp.c +0 -346
  144. data/ext/iodine/iodine_tcp.h +0 -13
  145. data/ext/iodine/iodine_tls.c +0 -261
  146. data/ext/iodine/mustache_parser.h +0 -1546
  147. data/ext/iodine/redis_engine.c +0 -957
  148. data/ext/iodine/redis_engine.h +0 -79
  149. data/ext/iodine/resp_parser.h +0 -317
  150. data/ext/iodine/websocket_parser.h +0 -506
  151. data/ext/iodine/websockets.c +0 -743
  152. data/ext/iodine/websockets.h +0 -185
  153. data/lib/iodine/connection.rb +0 -61
  154. data/lib/iodine/json.rb +0 -42
  155. data/lib/iodine/mustache.rb +0 -113
  156. data/lib/iodine/pubsub.rb +0 -55
  157. data/lib/iodine/rack_utils.rb +0 -43
  158. data/lib/iodine/tls.rb +0 -16
  159. data/lib/rack/handler/iodine.rb +0 -33
  160. data/logo.png +0 -0
@@ -1,6 +1,740 @@
1
- #ifndef H_IODINE_JSON_H
2
- #define H_IODINE_JSON_H
1
+ #ifndef H___IODINE_JSON___H
2
+ #define H___IODINE_JSON___H
3
+ #include "iodine.h"
3
4
 
4
- void iodine_init_json(void);
5
+ /* *****************************************************************************
6
+ Iodine JSON - Fast JSON Parsing and Stringification
5
7
 
6
- #endif
8
+ This module provides the Iodine::JSON Ruby module for JSON operations.
9
+ It's primarily used internally for WebSocket message serialization but
10
+ is also exposed as a public API.
11
+
12
+ Features:
13
+ - Fast JSON stringification (Ruby objects to JSON strings)
14
+ - JSON parsing (JSON strings to Ruby objects)
15
+ - Beautified/pretty-printed JSON output
16
+ - Handles nested arrays, hashes, strings, numbers, booleans, nil
17
+
18
+ Performance Notes:
19
+ - Stringification is fast (single memory copy from C to Ruby)
20
+ - Parsing is slower than alternatives (double copy: JSON->FIOBJ->Ruby)
21
+ - For production JSON work, consider using the 'oj' gem instead
22
+
23
+ Ruby API (Iodine::JSON):
24
+ - Iodine::JSON.parse(json_string) - Parse JSON to Ruby objects
25
+ - Iodine::JSON.stringify(object) - Convert Ruby object to JSON
26
+ - Iodine::JSON.dump(object) - Alias for stringify
27
+ - Iodine::JSON.beautify(object) - Pretty-printed JSON output
28
+ - Iodine::JSON.beautify_slow(object) - Alternative beautifier via FIOBJ
29
+ - Iodine::JSON.parse_slow(json) - Alternative parser via FIOBJ
30
+ ***************************************************************************** */
31
+
32
+ /* *****************************************************************************
33
+ JSON Stringifier - Ruby to JSON String Conversion
34
+ ***************************************************************************** */
35
+
36
+ static char *iodine_json_stringify_key(char *dest, VALUE tmp) {
37
+ /* Note: keys MUST be Strings */
38
+ switch (rb_type(tmp)) {
39
+ case RUBY_T_BIGNUM: /* Bignum (Windows: Integers >= 2^30) */
40
+ tmp = rb_big2str(tmp, 10);
41
+ goto string_key;
42
+ case RUBY_T_SYMBOL: tmp = rb_sym2str(tmp); /* fall through */
43
+ case RUBY_T_STRING:
44
+ string_key:
45
+ dest = fio_bstr_write(dest, "\"", 1);
46
+ dest =
47
+ fio_bstr_write_escape(dest, RSTRING_PTR(tmp), (size_t)RSTRING_LEN(tmp));
48
+ dest = fio_bstr_write(dest, "\"", 1);
49
+ break;
50
+ case RUBY_T_FIXNUM:
51
+ dest = fio_bstr_write2(dest,
52
+ FIO_STRING_WRITE_STR2("\"", 1),
53
+ FIO_STRING_WRITE_NUM(NUM2LL(tmp)),
54
+ FIO_STRING_WRITE_STR2("\"", 1));
55
+ break;
56
+ case RUBY_T_TRUE: dest = fio_bstr_write(dest, "\"true\"", 6); break;
57
+ case RUBY_T_FALSE: dest = fio_bstr_write(dest, "\"false\"", 7); break;
58
+ case RUBY_T_NIL: dest = fio_bstr_write(dest, "\"null\"", 6); break;
59
+ default:
60
+ tmp = rb_any_to_s(tmp);
61
+ if (RB_TYPE_P(tmp, RUBY_T_STRING))
62
+ goto string_key;
63
+ dest = fio_bstr_write(dest, "\"error\"", 7);
64
+ }
65
+ return dest;
66
+ }
67
+
68
+ static char *iodine_json_stringify2bstr(char *dest, VALUE o);
69
+
70
+ static int iodine_json_hash_each_callback(VALUE k, VALUE v, VALUE dest_) {
71
+ char **pdest = (char **)dest_;
72
+ *pdest = iodine_json_stringify_key(*pdest, k);
73
+ *pdest = fio_bstr_write(*pdest, ":", 1);
74
+ *pdest = iodine_json_stringify2bstr(*pdest, v);
75
+ *pdest = fio_bstr_write(*pdest, ",", 1);
76
+ return ST_CONTINUE;
77
+ }
78
+
79
+ /* Converts a VALUE to a fio_bstr */
80
+ static char *iodine_json_stringify2bstr(char *dest, VALUE o) {
81
+ size_t tmp;
82
+ switch (rb_type(o)) {
83
+ case RUBY_T_NIL: return (dest = fio_bstr_write(dest, "null", 4));
84
+ case RUBY_T_TRUE: return (dest = fio_bstr_write(dest, "true", 4));
85
+ case RUBY_T_FALSE: return (dest = fio_bstr_write(dest, "false", 5));
86
+ case RUBY_T_ARRAY:
87
+ tmp = rb_array_len(o);
88
+ if (!tmp) {
89
+ dest = fio_bstr_write(dest, "[]", 2);
90
+ return dest;
91
+ }
92
+ dest = fio_bstr_write(dest, "[", 1);
93
+ for (size_t i = 0; i < tmp; ++i) {
94
+ dest = iodine_json_stringify2bstr(dest, RARRAY_PTR(o)[i]);
95
+ dest = fio_bstr_write(dest, ",", 1);
96
+ }
97
+ dest[fio_bstr_len(dest) - 1] = ']';
98
+ return dest;
99
+ case RUBY_T_HASH:
100
+ tmp = rb_hash_size_num(o);
101
+ if (!tmp) {
102
+ dest = fio_bstr_write(dest, "{}", 2);
103
+ return dest;
104
+ }
105
+ dest = fio_bstr_write(dest, "{", 1);
106
+ rb_hash_foreach(o, iodine_json_hash_each_callback, (VALUE)(&dest));
107
+ dest[fio_bstr_len(dest) - 1] = '}';
108
+ return dest;
109
+ case RUBY_T_FIXNUM: return (dest = fio_bstr_write_i(dest, RB_NUM2LL(o)));
110
+ case RUBY_T_BIGNUM: /* Bignum (Windows: Integers >= 2^30) */
111
+ o = rb_big2str(o, 10);
112
+ return (dest =
113
+ fio_bstr_write(dest, RSTRING_PTR(o), (size_t)RSTRING_LEN(o)));
114
+ case RUBY_T_FLOAT: {
115
+ FIO_STR_INFO_TMP_VAR(buf, 232);
116
+ buf.len = fio_ftoa(buf.buf, RFLOAT_VALUE(o), 10);
117
+ dest = fio_bstr_write(dest, buf.buf, buf.len);
118
+ return dest;
119
+ }
120
+ case RUBY_T_SYMBOL: o = rb_sym2str(o); /* fall through */
121
+ case RUBY_T_STRING:
122
+ add_string:
123
+ dest = fio_bstr_write(dest, "\"", 1);
124
+ dest = fio_bstr_write_escape(dest, RSTRING_PTR(o), (size_t)RSTRING_LEN(o));
125
+ dest = fio_bstr_write(dest, "\"", 1);
126
+ return dest;
127
+ default:
128
+ if (1) {
129
+ VALUE tmp = rb_check_funcall(o, IODINE_TO_JSON_ID, 0, NULL);
130
+ if (tmp != RUBY_Qundef && RB_TYPE_P(tmp, RUBY_T_STRING))
131
+ return (dest = fio_bstr_write(dest,
132
+ RSTRING_PTR(tmp),
133
+ (size_t)RSTRING_LEN(tmp)));
134
+ o = rb_funcallv(o, IODINE_TO_S_ID, 0, NULL);
135
+ }
136
+ if (!RB_TYPE_P(o, RUBY_T_STRING)) {
137
+ FIO_LOG_ERROR("Iodine::JSON.stringify called with an object that doesn't "
138
+ "respond to #to_s.");
139
+ return (dest = fio_bstr_write(dest, "null", 4));
140
+ }
141
+ goto add_string;
142
+ }
143
+ }
144
+
145
+ /* *****************************************************************************
146
+ JSON Beautifier - Pretty-Printed JSON Output
147
+ ***************************************************************************** */
148
+
149
+ typedef struct {
150
+ char *o;
151
+ size_t t;
152
+ } iodine_json_beautify2bstr_s;
153
+
154
+ static char *iodine_json_beautify2bstr(iodine_json_beautify2bstr_s *dest,
155
+ VALUE o);
156
+
157
+ FIO_IFUNC void iodine_json_beautify2bstr_pad(iodine_json_beautify2bstr_s *d) {
158
+ if (!d->t)
159
+ return;
160
+ size_t len = fio_bstr_len(d->o);
161
+ d->o = fio_bstr_len_set(d->o, len + d->t + 1);
162
+ char *tmp = d->o + len;
163
+ *tmp++ = '\n';
164
+ FIO_MEMSET(tmp, '\t', d->t);
165
+ }
166
+
167
+ static int iodine_json_hash_each_beautify_callback(VALUE k,
168
+ VALUE v,
169
+ VALUE dest_) {
170
+ iodine_json_beautify2bstr_s *d = (iodine_json_beautify2bstr_s *)dest_;
171
+ iodine_json_beautify2bstr_pad(d);
172
+ d->o = iodine_json_stringify_key(d->o, k);
173
+ d->o = fio_bstr_write(d->o, ": ", 2);
174
+ iodine_json_beautify2bstr(d, v);
175
+ d->o = fio_bstr_write(d->o, ",", 1);
176
+ return ST_CONTINUE;
177
+ }
178
+
179
+ /* Converts a VALUE to a fio_bstr */
180
+ static char *iodine_json_beautify2bstr(iodine_json_beautify2bstr_s *d,
181
+ VALUE o) {
182
+ size_t tmp;
183
+ switch (rb_type(o)) {
184
+ case RUBY_T_NIL: return (d->o = fio_bstr_write(d->o, "null", 4));
185
+ case RUBY_T_TRUE: return (d->o = fio_bstr_write(d->o, "true", 4));
186
+ case RUBY_T_FALSE: return (d->o = fio_bstr_write(d->o, "false", 5));
187
+ case RUBY_T_ARRAY:
188
+ tmp = rb_array_len(o);
189
+ if (!tmp) {
190
+ d->o = fio_bstr_write(d->o, "[]", 2);
191
+ return d->o;
192
+ }
193
+ d->o = fio_bstr_write(d->o, "[", 1);
194
+ ++d->t;
195
+ for (size_t i = 0; i < tmp; ++i) {
196
+ iodine_json_beautify2bstr_pad(d);
197
+ iodine_json_beautify2bstr(d, RARRAY_PTR(o)[i]);
198
+ d->o = fio_bstr_write(d->o, ",", 1);
199
+ }
200
+ d->o[fio_bstr_len(d->o) - 1] = '\n';
201
+ --d->t;
202
+ iodine_json_beautify2bstr_pad(d);
203
+ d->o = fio_bstr_write(d->o, "]", 1);
204
+ return d->o;
205
+ case RUBY_T_HASH:
206
+ tmp = rb_hash_size_num(o);
207
+ if (!tmp) {
208
+ d->o = fio_bstr_write(d->o, "{}", 2);
209
+ return d->o;
210
+ }
211
+ d->o = fio_bstr_write(d->o, "{", 1);
212
+ ++d->t;
213
+ rb_hash_foreach(o, iodine_json_hash_each_beautify_callback, (VALUE)(d));
214
+ d->o[fio_bstr_len(d->o) - 1] = '\n';
215
+ --d->t;
216
+ iodine_json_beautify2bstr_pad(d);
217
+ d->o = fio_bstr_write(d->o, "}", 1);
218
+ return d->o;
219
+ case RUBY_T_FIXNUM: return (d->o = fio_bstr_write_i(d->o, RB_NUM2LL(o)));
220
+ case RUBY_T_BIGNUM: /* Bignum (Windows: Integers >= 2^30) */
221
+ o = rb_big2str(o, 10);
222
+ return (d->o =
223
+ fio_bstr_write(d->o, RSTRING_PTR(o), (size_t)RSTRING_LEN(o)));
224
+ case RUBY_T_FLOAT: {
225
+ FIO_STR_INFO_TMP_VAR(buf, 232);
226
+ buf.len = fio_ftoa(buf.buf, RFLOAT_VALUE(o), 10);
227
+ d->o = fio_bstr_write(d->o, buf.buf, buf.len);
228
+ return d->o;
229
+ }
230
+ case RUBY_T_SYMBOL: o = rb_sym2str(o); /* fall through */
231
+ case RUBY_T_STRING:
232
+ add_string:
233
+ d->o = fio_bstr_write(d->o, "\"", 1);
234
+ d->o = fio_bstr_write_escape(d->o, RSTRING_PTR(o), (size_t)RSTRING_LEN(o));
235
+ d->o = fio_bstr_write(d->o, "\"", 1);
236
+ return d->o;
237
+ default:
238
+ if (1) {
239
+ VALUE tmp = rb_check_funcall(o, IODINE_TO_JSON_ID, 0, NULL);
240
+ if (tmp != RUBY_Qundef && RB_TYPE_P(tmp, RUBY_T_STRING))
241
+ return (d->o = fio_bstr_write(d->o,
242
+ RSTRING_PTR(tmp),
243
+ (size_t)RSTRING_LEN(tmp)));
244
+ o = rb_funcallv(o, IODINE_TO_S_ID, 0, NULL);
245
+ if (RB_TYPE_P(o, RUBY_T_STRING))
246
+ goto add_string;
247
+ }
248
+ FIO_LOG_ERROR("Iodine::JSON.stringify called with an object that doesn't "
249
+ "respond to neither #to_json nor #to_s.");
250
+ return (d->o = fio_bstr_write(d->o, "null", 4));
251
+ }
252
+ }
253
+
254
+ /* *****************************************************************************
255
+ FIOBJ => Ruby Bridge - Convert facil.io Objects to Ruby
256
+ ***************************************************************************** */
257
+
258
+ typedef struct iodine_fiobj2ruby_task_s {
259
+ VALUE out;
260
+ } iodine_fiobj2ruby_task_s;
261
+
262
+ static VALUE iodine_fiobj2ruby(FIOBJ o);
263
+
264
+ static int iodine_fiobj2ruby_array_task(fiobj_array_each_s *e) {
265
+ rb_ary_push((VALUE)e->udata, iodine_fiobj2ruby(e->value));
266
+ return 0;
267
+ }
268
+ static int iodine_fiobj2ruby_hash_task(fiobj_hash_each_s *e) {
269
+ VALUE k = iodine_fiobj2ruby(e->key);
270
+ VALUE v = iodine_fiobj2ruby(e->value);
271
+ rb_hash_aset((VALUE)e->udata, k, v);
272
+ return 0;
273
+ }
274
+
275
+ /**
276
+ * Converts a FIOBJ (facil.io object) to a Ruby VALUE.
277
+ *
278
+ * Recursively converts FIOBJ types to their Ruby equivalents:
279
+ * - FIOBJ_T_TRUE/FALSE -> Qtrue/Qfalse
280
+ * - FIOBJ_T_NUMBER -> Fixnum/Bignum
281
+ * - FIOBJ_T_FLOAT -> Float
282
+ * - FIOBJ_T_STRING -> String
283
+ * - FIOBJ_T_ARRAY -> Array
284
+ * - FIOBJ_T_HASH -> Hash
285
+ * - FIOBJ_T_NULL/INVALID -> Qnil
286
+ *
287
+ * @param o The FIOBJ to convert
288
+ * @return Ruby VALUE equivalent
289
+ *
290
+ * @note Does NOT place VALUE in STORE automatically.
291
+ */
292
+ static VALUE iodine_fiobj2ruby(FIOBJ o) {
293
+ VALUE r;
294
+ switch (FIOBJ_TYPE(o)) {
295
+ case FIOBJ_T_TRUE: return Qtrue;
296
+ case FIOBJ_T_FALSE: return Qfalse;
297
+ case FIOBJ_T_NUMBER: return RB_LL2NUM(fiobj_num2i(o));
298
+ case FIOBJ_T_FLOAT: return rb_float_new(fiobj_float2f(o));
299
+ case FIOBJ_T_STRING: return rb_str_new(fiobj_str_ptr(o), fiobj_str_len(o));
300
+ case FIOBJ_T_ARRAY:
301
+ STORE.gc_stop();
302
+ r = rb_ary_new_capa(fiobj_array_count(o));
303
+ fiobj_array_each(o, iodine_fiobj2ruby_array_task, (void *)r, 0);
304
+ STORE.gc_start();
305
+ return r;
306
+ case FIOBJ_T_HASH:
307
+ STORE.gc_stop();
308
+ r = rb_hash_new();
309
+ fiobj_hash_each(o, iodine_fiobj2ruby_hash_task, (void *)r, 0);
310
+ STORE.gc_start();
311
+ return r;
312
+ // case FIOBJ_T_NULL: /* fall through */
313
+ // case FIOBJ_T_INVALID: /* fall through */
314
+ default: return Qnil;
315
+ }
316
+ }
317
+
318
+ /* *****************************************************************************
319
+ Ruby => FIOBJ Bridge - Convert Ruby Objects to facil.io Objects
320
+ ***************************************************************************** */
321
+
322
+ typedef struct iodine_ruby2fiobj_task_s {
323
+ FIOBJ out;
324
+ } iodine_ruby2fiobj_task_s;
325
+
326
+ static FIOBJ iodine_ruby2fiobj(VALUE o);
327
+
328
+ static int iodine_ruby2fiobj_hash_each_task(VALUE n, VALUE v, VALUE h_) {
329
+ FIOBJ h = (FIOBJ)h_;
330
+ FIOBJ key = iodine_ruby2fiobj(n);
331
+ fiobj_hash_set(h, key, iodine_ruby2fiobj(v), NULL);
332
+ fiobj_free(key); /* by default, keys aren't owned by the hash */
333
+ return ST_CONTINUE;
334
+ }
335
+
336
+ /**
337
+ * Converts a Ruby VALUE to a FIOBJ (facil.io object).
338
+ *
339
+ * Recursively converts Ruby types to their FIOBJ equivalents:
340
+ * - Qtrue/Qfalse -> fiobj_true()/fiobj_false()
341
+ * - Fixnum -> fiobj_num_new()
342
+ * - Float -> fiobj_float_new()
343
+ * - Symbol/String -> fiobj_str_new_cstr()
344
+ * - Array -> fiobj_array_new()
345
+ * - Hash -> fiobj_hash_new()
346
+ * - nil/undef -> fiobj_null()
347
+ * - Other -> calls #to_json or #to_s
348
+ *
349
+ * @param o The Ruby VALUE to convert
350
+ * @return FIOBJ equivalent
351
+ *
352
+ * @note Does NOT place VALUE in STORE automatically.
353
+ */
354
+ static FIOBJ iodine_ruby2fiobj(VALUE o) {
355
+ FIOBJ r;
356
+ VALUE tmp;
357
+ switch (rb_type(o)) {
358
+ case RUBY_T_TRUE: return fiobj_true();
359
+ case RUBY_T_FALSE: return fiobj_false();
360
+ case RUBY_T_FIXNUM: return fiobj_num_new((intptr_t)RB_NUM2LL(o));
361
+ case RUBY_T_BIGNUM: /* Bignum (Windows: Integers >= 2^30) */
362
+ if (1) {
363
+ size_t c = 0;
364
+ o = rb_big2str(o, 10);
365
+ return fiobj_json_parse((fio_str_info_s)IODINE_RSTR_INFO(o), &c);
366
+ }
367
+ case RUBY_T_FLOAT: return fiobj_float_new(rb_float_value(o));
368
+ case RUBY_T_SYMBOL: o = rb_sym_to_s(o); /* fall through */
369
+ case RUBY_T_STRING: return fiobj_str_new_cstr(RSTRING_PTR(o), RSTRING_LEN(o));
370
+ case RUBY_T_ARRAY:
371
+ if (1) {
372
+ const size_t alen = rb_array_len(o);
373
+ r = fiobj_array_new();
374
+ if (alen)
375
+ fiobj_array_reserve(r, (int64_t)alen);
376
+ for (size_t i = 0; i < alen; ++i)
377
+ fiobj_array_push(r, iodine_ruby2fiobj(RARRAY_PTR(o)[i]));
378
+ return r;
379
+ }
380
+ case RUBY_T_HASH:
381
+ r = fiobj_hash_new();
382
+ if (rb_hash_size_num(o))
383
+ fiobj_hash_reserve(r, rb_hash_size_num(o));
384
+ rb_hash_foreach(o, iodine_ruby2fiobj_hash_each_task, (VALUE)r);
385
+ return r;
386
+ case RUBY_T_NIL: /* fall through */
387
+ case RUBY_T_UNDEF: /* fall through */
388
+ case RUBY_T_NONE: return fiobj_null();
389
+ default:
390
+ tmp = rb_check_funcall(o, IODINE_TO_JSON_ID, 0, NULL);
391
+ if (tmp != RUBY_Qundef && RB_TYPE_P(tmp, RUBY_T_STRING)) {
392
+ size_t c = 0;
393
+ r = fiobj_json_parse((fio_str_info_s)IODINE_RSTR_INFO(tmp), &c);
394
+ return r;
395
+ }
396
+ o = rb_any_to_s(o);
397
+ if (RB_TYPE_P(o, RUBY_T_STRING))
398
+ return fiobj_str_new_cstr(RSTRING_PTR(o), RSTRING_LEN(o));
399
+ return fiobj_null();
400
+ }
401
+ }
402
+
403
+ /* *****************************************************************************
404
+ JSON Parser (Indirect) - Parse via FIOBJ intermediate
405
+ ***************************************************************************** */
406
+
407
+ /**
408
+ * Parses a JSON string to Ruby objects via FIOBJ intermediate.
409
+ *
410
+ * This is the slower parsing path that:
411
+ * 1. Parses JSON string to FIOBJ tree
412
+ * 2. Converts FIOBJ tree to Ruby objects
413
+ *
414
+ * @param self The Iodine::JSON module
415
+ * @param rstr The JSON string to parse (String)
416
+ * @return Ruby object tree (Hash, Array, String, etc.)
417
+ *
418
+ * Ruby: Iodine::JSON.parse_slow(json_string)
419
+ */
420
+ static VALUE iodine_json_parse_indirect(VALUE self, VALUE rstr) {
421
+ VALUE r = Qnil;
422
+ rb_check_type(rstr, RUBY_T_STRING);
423
+ size_t consumed = 0;
424
+ FIOBJ tmp =
425
+ fiobj_json_parse((fio_str_info_s)IODINE_RSTR_INFO(rstr), &consumed);
426
+ STORE.gc_stop();
427
+ r = iodine_fiobj2ruby(tmp);
428
+ STORE.gc_start();
429
+ fiobj_free(tmp);
430
+ return r;
431
+ }
432
+
433
+ /* *****************************************************************************
434
+ JSON Parser (Direct) - Parse Directly to Ruby Objects
435
+
436
+ This parser uses facil.io's streaming JSON parser with callbacks that
437
+ create Ruby objects directly, avoiding the FIOBJ intermediate step.
438
+ This is the faster parsing path used by Iodine::JSON.parse().
439
+ ***************************************************************************** */
440
+
441
+ /** JSON parser callback implementations for direct Ruby object creation */
442
+ /** NULL object was detected. Returns new object as `void *`. */
443
+ FIO_SFUNC void *iodine___json_on_null(void *udata) {
444
+ (void)udata;
445
+ return (void *)Qnil;
446
+ }
447
+ /** TRUE object was detected. Returns new object as `void *`. */
448
+ FIO_SFUNC void *iodine___json_on_true(void *udata) {
449
+ (void)udata;
450
+ return (void *)Qtrue;
451
+ }
452
+ /** FALSE object was detected. Returns new object as `void *`. */
453
+ FIO_SFUNC void *iodine___json_on_false(void *udata) {
454
+ (void)udata;
455
+ return (void *)Qfalse;
456
+ }
457
+ /** Number was detected (long long). Returns new object as `void *`. */
458
+ FIO_SFUNC void *iodine___json_on_number(void *udata, int64_t i) {
459
+ (void)udata;
460
+ return (void *)(LL2NUM(((long long)i)));
461
+ }
462
+ /** Float was detected (double).Returns new object as `void *`. */
463
+ FIO_SFUNC void *iodine___json_on_float(void *udata, double f) {
464
+ (void)udata;
465
+ return (void *)DBL2NUM(f);
466
+ }
467
+ /** String was detected (int / float). update `pos` to point at ending */
468
+ FIO_SFUNC void *iodine___json_on_string(void *udata,
469
+ const void *start,
470
+ size_t len) {
471
+ (void)udata;
472
+ VALUE str;
473
+ if (len < 4096) {
474
+ FIO_STR_INFO_TMP_VAR(buf, 4096);
475
+ fio_string_write_unescape(&buf, NULL, start, len);
476
+ str = rb_str_new(buf.buf, buf.len);
477
+ } else {
478
+ char *tmp = fio_bstr_write_unescape(NULL, start, len);
479
+ str = rb_str_new(tmp, fio_bstr_len(tmp));
480
+ fio_bstr_free(tmp);
481
+ }
482
+ return (void *)str;
483
+ }
484
+ FIO_SFUNC void *iodine___json_on_string_simple(void *udata,
485
+ const void *start,
486
+ size_t len) {
487
+ (void)udata;
488
+ VALUE str = rb_str_new((const char *)start, len);
489
+ return (void *)str;
490
+ }
491
+ /** Dictionary was detected. Returns ctx to hash map or NULL on error. */
492
+ FIO_SFUNC void *iodine___json_on_map(void *udata, void *ctx, void *at) {
493
+ (void)udata, (void)ctx, (void)at;
494
+ VALUE map = rb_hash_new();
495
+ return (void *)map;
496
+ }
497
+ /** Array was detected. Returns ctx to array or NULL on error. */
498
+ FIO_SFUNC void *iodine___json_on_array(void *udata, void *ctx, void *at) {
499
+ (void)udata, (void)ctx, (void)at;
500
+ VALUE ary = rb_ary_new();
501
+ return (void *)ary;
502
+ }
503
+ /** Map entry detected. Returns non-zero on error. */
504
+ FIO_SFUNC int iodine___json_map_push(void *udata,
505
+ void *ctx,
506
+ void *key,
507
+ void *val) {
508
+ (void)udata;
509
+ rb_hash_aset((VALUE)ctx, (VALUE)key, (VALUE)val);
510
+ return 0;
511
+ }
512
+ /** Array entry detected. Returns non-zero on error. */
513
+ FIO_SFUNC int iodine___json_array_push(void *udata, void *ctx, void *val) {
514
+ (void)udata;
515
+ rb_ary_push((VALUE)ctx, (VALUE)val);
516
+ return 0;
517
+ }
518
+ /** Called for the `key` element in case of error or NULL value. */
519
+ FIO_SFUNC void iodine___json_free_unused_object(void *udata, void *ctx) {
520
+ (void)udata, (void)ctx;
521
+ }
522
+ /** the JSON parsing encountered an error - what to do with ctx? */
523
+ FIO_SFUNC void *iodine___json_on_error(void *udata, void *ctx) {
524
+ (void)udata, (void)ctx;
525
+ return (void *)Qnil;
526
+ }
527
+
528
+ static fio_json_parser_callbacks_s IODINE_JSON_PARSER_CALLBACKS = {
529
+ .on_null = iodine___json_on_null,
530
+ .on_true = iodine___json_on_true,
531
+ .on_false = iodine___json_on_false,
532
+ .on_number = iodine___json_on_number,
533
+ .on_float = iodine___json_on_float,
534
+ .on_string = iodine___json_on_string,
535
+ .on_string_simple = iodine___json_on_string_simple,
536
+ .on_map = iodine___json_on_map,
537
+ .on_array = iodine___json_on_array,
538
+ .map_push = iodine___json_map_push,
539
+ .array_push = iodine___json_array_push,
540
+ .free_unused_object = iodine___json_free_unused_object,
541
+ .on_error = iodine___json_on_error,
542
+ };
543
+
544
+ /* *****************************************************************************
545
+ API - Public Ruby Methods
546
+ ***************************************************************************** */
547
+
548
+ /**
549
+ * Parses a JSON string to Ruby objects (fast path).
550
+ *
551
+ * Uses the direct parser that creates Ruby objects without
552
+ * an intermediate FIOBJ representation.
553
+ *
554
+ * @param self The Iodine::JSON module
555
+ * @param rstr The JSON string to parse (String)
556
+ * @return Ruby object tree (Hash, Array, String, etc.)
557
+ *
558
+ * Ruby: Iodine::JSON.parse(json_string)
559
+ */
560
+ static VALUE iodine_json_parse(VALUE self, VALUE rstr) {
561
+ VALUE r = Qnil;
562
+ rb_check_type(rstr, RUBY_T_STRING);
563
+ STORE.gc_stop();
564
+ fio_json_result_s result = fio_json_parse(&IODINE_JSON_PARSER_CALLBACKS,
565
+ NULL,
566
+ RSTRING_PTR(rstr),
567
+ RSTRING_LEN(rstr));
568
+ STORE.gc_start();
569
+ r = (VALUE)(result.ctx);
570
+ STORE.cache(r);
571
+ rb_str_buf_new(1); /* force GC to run if Ruby needs it */
572
+ return r;
573
+ }
574
+
575
+ /**
576
+ * Converts a Ruby object to a JSON string.
577
+ *
578
+ * Handles all standard Ruby types (Hash, Array, String, Numeric,
579
+ * true, false, nil). For other objects, calls #to_json or #to_s.
580
+ *
581
+ * @param self The Iodine::JSON module
582
+ * @param object The Ruby object to stringify
583
+ * @return JSON string representation
584
+ *
585
+ * Ruby: Iodine::JSON.stringify(object)
586
+ * Iodine::JSON.dump(object) # alias
587
+ */
588
+ static VALUE iodine_json_stringify(VALUE self, VALUE object) {
589
+ VALUE r = Qnil;
590
+ char *str = fio_bstr_reserve(NULL, ((size_t)1 << 12) - 64);
591
+ str = iodine_json_stringify2bstr(str, object);
592
+ r = rb_str_new(str, (long)fio_bstr_len(str));
593
+ fio_bstr_free(str);
594
+ return r;
595
+ }
596
+
597
+ /**
598
+ * Converts a Ruby object to a pretty-printed JSON string (slow path).
599
+ *
600
+ * Uses FIOBJ intermediate for formatting. Slower but produces
601
+ * nicely indented output.
602
+ *
603
+ * @param self The Iodine::JSON module
604
+ * @param object The Ruby object to stringify
605
+ * @return Pretty-printed JSON string
606
+ *
607
+ * Ruby: Iodine::JSON.beautify_slow(object)
608
+ */
609
+ static VALUE iodine_json_pretty(VALUE self, VALUE object) {
610
+ VALUE r = Qnil;
611
+ FIOBJ o = iodine_ruby2fiobj(object);
612
+ FIOBJ out = fiobj2json(FIOBJ_T_INVALID, o, 1);
613
+ fiobj_free(o);
614
+ r = rb_str_new(fiobj_str_ptr(out), (long)fiobj_str_len(out));
615
+ fiobj_free(out);
616
+ return r;
617
+ }
618
+
619
+ /**
620
+ * Converts a Ruby object to a pretty-printed JSON string (fast path).
621
+ *
622
+ * Uses direct stringification with indentation. Faster than
623
+ * beautify_slow but produces similar output.
624
+ *
625
+ * @param self The Iodine::JSON module
626
+ * @param object The Ruby object to stringify
627
+ * @return Pretty-printed JSON string with newlines and tabs
628
+ *
629
+ * Ruby: Iodine::JSON.beautify(object)
630
+ */
631
+ static VALUE iodine_json_beautify(VALUE self, VALUE object) {
632
+ VALUE r = Qnil;
633
+ iodine_json_beautify2bstr_s dest = {
634
+ .o = fio_bstr_reserve(NULL, ((size_t)1 << 12) - 64),
635
+ };
636
+ iodine_json_beautify2bstr(&dest, object);
637
+ r = rb_str_new(dest.o, (long)fio_bstr_len(dest.o));
638
+ fio_bstr_free(dest.o);
639
+ return r;
640
+ }
641
+
642
+ /** Initialize Iodine::JSON */ // clang-format off
643
+ /**
644
+ Iodine::JSON exposes the {Iodine::Connection#write} fallback behavior when called with non-String objects.
645
+
646
+ The fallback behavior is similar to (though faster than) calling:
647
+
648
+ client.write(Iodine::JSON.stringify(data))
649
+
650
+ If you want to work with JSON, consider using the `oj` gem.
651
+
652
+ This API is mostly to test for Iodine JSON input/output errors and reflects what the C layer sees.
653
+
654
+ ## Performance
655
+
656
+ Performance... could be better.
657
+
658
+ Converting Ruby objects into a JSON String (Strinfigying) should be fast even though the String data is copied twice, once to C and then to Ruby.
659
+
660
+ However, Converting a JSON object into Ruby Objects is currently slow and it is better to use the `oj` gem or even the Ruby builtin parser.
661
+
662
+ The reason is simple - the implementation is designed to create C objects (C Hash Maps, C Arrays, etc'), not Ruby objects. When converting from a String to Ruby Objevts, the data is copied twice, once to C and then to Ruby.
663
+
664
+ This especially effects parsing, where more objects are allocated,
665
+ whereas {Iodine::JSON.stringify} only (re)copies the String data which is a single continuous block of memory.
666
+
667
+ That's why {Iodine::JSON.stringify} is significantly faster than the Ruby `object.to_json` approach,
668
+ yet slower than `JSON.parse(json_string)`.
669
+
670
+ require 'oj' rescue nil
671
+ require 'benchmark/ips'
672
+ require 'json'
673
+ require 'iodine'
674
+
675
+ def benchmark_json
676
+ # make a big data store with nothings
677
+ data_1000 = []
678
+ 1000.times do
679
+ tmp = {f: rand() };
680
+ tmp[:i] = (tmp[:f] * 1000000).to_i
681
+ tmp[:str] = tmp[:i].to_s
682
+ tmp[:sym] = tmp[:str].to_sym
683
+ tmp[:ary] = []
684
+ tmp[:ary_empty] = []
685
+ tmp[:hash_empty] = Hash.new
686
+ 100.times {|i| tmp[:ary] << i }
687
+ data_1000 << tmp
688
+ end
689
+ 3.times do
690
+ json_string = data_1000.to_json
691
+ puts "-----"
692
+ puts "Benchmark #{data_1000.length} item tree, and #{json_string.length} bytes of JSON"
693
+ # benchmark stringification
694
+ Benchmark.ips do |x|
695
+ x.report(" Ruby obj.to_json") do |times|
696
+ data_1000.to_json
697
+ end
698
+ x.report("Iodine::JSON.stringify") do |times|
699
+ Iodine::JSON.stringify(data_1000)
700
+ end
701
+ if(defined?(Oj))
702
+ x.report(" Oj.dump") do |times|
703
+ Oj.dump(data_1000)
704
+ end
705
+ end
706
+ x.compare!
707
+ end ; nil
708
+ # benchmark parsing
709
+ Benchmark.ips do |x|
710
+ x.report(" Ruby JSON.parse") do |times|
711
+ JSON.parse(json_string)
712
+ end
713
+ x.report("Iodine::JSON.parse") do |times|
714
+ Iodine::JSON.parse(json_string)
715
+ end
716
+ if(defined?(Oj))
717
+ x.report(" Oj.load") do |times|
718
+ Oj.load(json_string)
719
+ end
720
+ end
721
+ x.compare!
722
+ end
723
+ data_1000 = data_1000.slice(0, (data_1000.length / 10))
724
+ nil
725
+ end
726
+ end
727
+
728
+ benchmark_json
729
+
730
+ */
731
+ static void Init_Iodine_JSON(void) {
732
+ VALUE m = rb_define_module_under(iodine_rb_IODINE, "JSON"); // clang-format on
733
+ rb_define_singleton_method(m, "parse", iodine_json_parse, 1);
734
+ rb_define_singleton_method(m, "parse_slow", iodine_json_parse_indirect, 1);
735
+ rb_define_singleton_method(m, "stringify", iodine_json_stringify, 1);
736
+ rb_define_singleton_method(m, "beautify_slow", iodine_json_pretty, 1);
737
+ rb_define_singleton_method(m, "beautify", iodine_json_beautify, 1);
738
+ rb_define_singleton_method(m, "dump", iodine_json_stringify, 1);
739
+ }
740
+ #endif /* H___IODINE_JSON___H */