quickjs 0.16.0 → 0.17.0.pre
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/README.md +46 -0
- data/ext/quickjsrb/quickjsrb.c +269 -48
- data/ext/quickjsrb/quickjsrb.h +27 -2
- data/ext/quickjsrb/vendor/polyfill-intl-en.min.js +4 -3
- data/lib/quickjs/function.rb +1 -11
- data/lib/quickjs/runnable.rb +23 -0
- data/lib/quickjs/version.rb +1 -1
- data/lib/quickjs.rb +16 -0
- data/polyfills/package-lock.json +126 -142
- data/polyfills/package.json +1 -1
- data/sig/quickjs.rbs +17 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b233c0e106654a50f4af274fef6a4dd77f0b53cc0a703473cda844da6ba3763
|
|
4
|
+
data.tar.gz: 50dfeaf56422ee74e8cb8711a58e1678c8141ac4bb7256a4d8861fa1a9415c93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f77f8b802055b1ec67ad9b4f45fb54cbdb1349a473457572c1c1b72115283bed9b99b15029e101256147ce0bbc72b64371fcbdd0ede6e8e9ba5e8d4a0bb0376
|
|
7
|
+
data.tar.gz: 9387e82c41c0242caa67c16a67b48a204c32512b116c5e4cc61d83e892bec053393facaf7ac2553642b876cacecb3622ddb37a846fd1eea959c21ea7df70b569
|
data/README.md
CHANGED
|
@@ -85,6 +85,21 @@ vm.eval_code('a.b = "d";')
|
|
|
85
85
|
vm.eval_code('a.b;') #=> "d"
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
#### `Quickjs::VM#compile`: 🚀 Cache parsed bundles as a `Quickjs::Runnable`
|
|
89
|
+
|
|
90
|
+
Parsing large JS bundles is the dominant cost of a fresh evaluation. `compile` parses once and returns a `Quickjs::Runnable` wrapping the serialized bytecode; `run(on:)` executes it on any VM of the same QuickJS build, skipping the parser. Useful when the same bundle is evaluated repeatedly across short-lived VMs (test environments, page-per-VM web emulators).
|
|
91
|
+
|
|
92
|
+
```rb
|
|
93
|
+
runnable = Quickjs::VM.new.compile(File.read('big_bundle.js'), filename: 'big_bundle.js')
|
|
94
|
+
|
|
95
|
+
vm = Quickjs::VM.new
|
|
96
|
+
runnable.run(on: vm) # use the given VM (no parse cost)
|
|
97
|
+
runnable.run # spin up a fresh VM with default options
|
|
98
|
+
runnable.run(on: { features: [::Quickjs::POLYFILL_INTL] }) # ad-hoc VM with options
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`Runnable#to_s` returns the underlying bytecode as a frozen ASCII-8BIT `String`, suitable for caching to memory or disk. `Quickjs::Runnable.new(bytecode_string)` reconstructs a `Runnable` from that blob — validation happens lazily at `run` time, so a corrupt or wrong-build blob surfaces as `Quickjs::RuntimeError` when executed. The bytecode format is tied to the QuickJS build, so include the gem version in your cache key if you persist across upgrades.
|
|
102
|
+
|
|
88
103
|
#### `Quickjs::VM#call`: ⚡ Call a JS function directly with Ruby arguments
|
|
89
104
|
|
|
90
105
|
```rb
|
|
@@ -213,6 +228,37 @@ vm.eval_code('console.log("hello", 42)')
|
|
|
213
228
|
# log.raw #=> Array of raw Ruby values
|
|
214
229
|
```
|
|
215
230
|
|
|
231
|
+
#### Memory management: 🔍 Inspect and control VM memory
|
|
232
|
+
|
|
233
|
+
```rb
|
|
234
|
+
vm = Quickjs::VM.new
|
|
235
|
+
|
|
236
|
+
vm.memory_usage
|
|
237
|
+
# => { malloc_size: Integer, malloc_limit: Integer, memory_used_size: Integer,
|
|
238
|
+
# atom_count: Integer, str_count: Integer, obj_count: Integer,
|
|
239
|
+
# prop_count: Integer, shape_count: Integer,
|
|
240
|
+
# js_func_count: Integer, js_func_code_size: Integer,
|
|
241
|
+
# c_func_count: Integer, array_count: Integer }
|
|
242
|
+
|
|
243
|
+
vm.gc! # trigger a QuickJS GC cycle; returns nil
|
|
244
|
+
|
|
245
|
+
vm.memory_poisoned? #=> false (true once the VM has hit out-of-memory)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
When the JS heap exhausts its memory limit, QuickJS enters a fragile state where further evaluation can segfault the process. `memory_poisoned?` flips to `true` after such an event, and subsequent `eval_code` / `call` calls raise `Quickjs::RuntimeError` immediately instead of risking a crash. Rescue it and recreate the VM.
|
|
249
|
+
|
|
250
|
+
```rb
|
|
251
|
+
vm = Quickjs::VM.new(memory_limit: 256 * 1024 * 1024)
|
|
252
|
+
|
|
253
|
+
begin
|
|
254
|
+
vm.eval_code(js)
|
|
255
|
+
rescue Quickjs::RuntimeError => e
|
|
256
|
+
raise unless vm.memory_poisoned?
|
|
257
|
+
vm = Quickjs::VM.new(memory_limit: 256 * 1024 * 1024)
|
|
258
|
+
retry
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
216
262
|
### Value Conversion
|
|
217
263
|
|
|
218
264
|
| JavaScript | | Ruby | Note |
|
data/ext/quickjsrb/quickjsrb.c
CHANGED
|
@@ -28,6 +28,10 @@ static int dispatch_log(VMData *data, const char *severity, VALUE r_row);
|
|
|
28
28
|
|
|
29
29
|
JSValue to_js_value(JSContext *ctx, VALUE r_value);
|
|
30
30
|
VALUE to_rb_value(JSContext *ctx, JSValue j_val);
|
|
31
|
+
static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited);
|
|
32
|
+
static VALUE vm_m_memoryUsage(VALUE r_self);
|
|
33
|
+
static VALUE vm_m_runGC(VALUE r_self);
|
|
34
|
+
static VALUE vm_m_memoryPoisoned(VALUE r_self);
|
|
31
35
|
|
|
32
36
|
JSValue j_error_from_ruby_error(JSContext *ctx, VALUE r_error)
|
|
33
37
|
{
|
|
@@ -187,12 +191,24 @@ VALUE to_r_json(JSContext *ctx, JSValue j_val)
|
|
|
187
191
|
{
|
|
188
192
|
JSValue j_stringified = JS_JSONStringify(ctx, j_val, JS_UNDEFINED, JS_UNDEFINED);
|
|
189
193
|
|
|
194
|
+
// JSON.stringify throws on circular structures (e.g. jQuery objects'
|
|
195
|
+
// prevObject chain). Clear the pending exception so it doesn't poison
|
|
196
|
+
// subsequent eval, and return nil so callers fall through to "couldn't
|
|
197
|
+
// parse" handling rather than crashing on rb_str_new2(NULL) below.
|
|
198
|
+
if (JS_IsException(j_stringified))
|
|
199
|
+
{
|
|
200
|
+
JS_FreeValue(ctx, j_stringified);
|
|
201
|
+
JSValue j_pending = JS_GetException(ctx);
|
|
202
|
+
JS_FreeValue(ctx, j_pending);
|
|
203
|
+
return Qnil;
|
|
204
|
+
}
|
|
205
|
+
|
|
190
206
|
const char *msg = JS_ToCString(ctx, j_stringified);
|
|
207
|
+
JS_FreeValue(ctx, j_stringified);
|
|
208
|
+
if (msg == NULL)
|
|
209
|
+
return Qnil;
|
|
191
210
|
VALUE r_str = rb_str_new2(msg);
|
|
192
211
|
JS_FreeCString(ctx, msg);
|
|
193
|
-
|
|
194
|
-
JS_FreeValue(ctx, j_stringified);
|
|
195
|
-
|
|
196
212
|
return r_str;
|
|
197
213
|
}
|
|
198
214
|
|
|
@@ -212,7 +228,7 @@ static int js_is_plain_object(JSContext *ctx, JSValue j_val)
|
|
|
212
228
|
return result;
|
|
213
229
|
}
|
|
214
230
|
|
|
215
|
-
static VALUE js_array_to_rb(JSContext *ctx, JSValue j_val)
|
|
231
|
+
static VALUE js_array_to_rb(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
216
232
|
{
|
|
217
233
|
JSValue j_length = JS_GetPropertyStr(ctx, j_val, "length");
|
|
218
234
|
uint32_t length = 0;
|
|
@@ -223,13 +239,13 @@ static VALUE js_array_to_rb(JSContext *ctx, JSValue j_val)
|
|
|
223
239
|
for (uint32_t i = 0; i < length; i++)
|
|
224
240
|
{
|
|
225
241
|
JSValue j_elem = JS_GetPropertyUint32(ctx, j_val, i);
|
|
226
|
-
rb_ary_push(r_array,
|
|
242
|
+
rb_ary_push(r_array, to_rb_value_inner(ctx, j_elem, r_visited));
|
|
227
243
|
JS_FreeValue(ctx, j_elem);
|
|
228
244
|
}
|
|
229
245
|
return r_array;
|
|
230
246
|
}
|
|
231
247
|
|
|
232
|
-
static VALUE js_plain_object_to_rb(JSContext *ctx, JSValue j_val)
|
|
248
|
+
static VALUE js_plain_object_to_rb(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
233
249
|
{
|
|
234
250
|
JSPropertyEnum *ptab;
|
|
235
251
|
uint32_t plen;
|
|
@@ -241,7 +257,7 @@ static VALUE js_plain_object_to_rb(JSContext *ctx, JSValue j_val)
|
|
|
241
257
|
{
|
|
242
258
|
const char *key = JS_AtomToCString(ctx, ptab[i].atom);
|
|
243
259
|
JSValue j_prop = JS_GetProperty(ctx, j_val, ptab[i].atom);
|
|
244
|
-
rb_hash_aset(r_hash, rb_str_new2(key),
|
|
260
|
+
rb_hash_aset(r_hash, rb_str_new2(key), to_rb_value_inner(ctx, j_prop, r_visited));
|
|
245
261
|
JS_FreeCString(ctx, key);
|
|
246
262
|
JS_FreeValue(ctx, j_prop);
|
|
247
263
|
}
|
|
@@ -250,6 +266,11 @@ static VALUE js_plain_object_to_rb(JSContext *ctx, JSValue j_val)
|
|
|
250
266
|
}
|
|
251
267
|
|
|
252
268
|
VALUE to_rb_value(JSContext *ctx, JSValue j_val)
|
|
269
|
+
{
|
|
270
|
+
return to_rb_value_inner(ctx, j_val, Qnil);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
253
274
|
{
|
|
254
275
|
switch (JS_VALUE_GET_NORM_TAG(j_val))
|
|
255
276
|
{
|
|
@@ -270,7 +291,11 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
|
|
|
270
291
|
return JS_ToBool(ctx, j_val) > 0 ? Qtrue : Qfalse;
|
|
271
292
|
}
|
|
272
293
|
case JS_TAG_STRING:
|
|
294
|
+
case JS_TAG_STRING_ROPE:
|
|
273
295
|
{
|
|
296
|
+
// QuickJS keeps long `s += chunk` chains as a rope (JS_TAG_STRING_ROPE)
|
|
297
|
+
// until something materialises them. JS_ToCStringLen flattens ropes
|
|
298
|
+
// transparently, so both tags share the same conversion path.
|
|
274
299
|
size_t len;
|
|
275
300
|
const char *str = JS_ToCStringLen(ctx, &len, j_val);
|
|
276
301
|
if (str == NULL)
|
|
@@ -340,30 +365,38 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
|
|
|
340
365
|
return r_maybe_file;
|
|
341
366
|
}
|
|
342
367
|
|
|
368
|
+
// Below this point, conversion recurses into own properties / elements
|
|
369
|
+
// via to_rb_value_inner. Track JS object pointers to break cycles —
|
|
370
|
+
// re-entering the same object returns nil instead of blowing the stack.
|
|
371
|
+
if (NIL_P(r_visited))
|
|
372
|
+
r_visited = rb_hash_new();
|
|
373
|
+
VALUE r_visit_key = ULL2NUM((uintptr_t)JS_VALUE_GET_PTR(j_val));
|
|
374
|
+
if (RTEST(rb_hash_lookup(r_visited, r_visit_key)))
|
|
375
|
+
return Qnil;
|
|
376
|
+
rb_hash_aset(r_visited, r_visit_key, Qtrue);
|
|
377
|
+
|
|
343
378
|
if (JS_IsArray(ctx, j_val))
|
|
344
|
-
return js_array_to_rb(ctx, j_val);
|
|
379
|
+
return js_array_to_rb(ctx, j_val, r_visited);
|
|
345
380
|
|
|
346
381
|
if (js_is_plain_object(ctx, j_val))
|
|
347
|
-
return js_plain_object_to_rb(ctx, j_val);
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
int couldntParse;
|
|
358
|
-
VALUE r_result = rb_protect(r_try_json_parse, r_str, &couldntParse);
|
|
359
|
-
if (couldntParse)
|
|
360
|
-
{
|
|
361
|
-
return Qnil;
|
|
362
|
-
}
|
|
363
|
-
else
|
|
382
|
+
return js_plain_object_to_rb(ctx, j_val, r_visited);
|
|
383
|
+
|
|
384
|
+
// Non-plain objects (Date, RegExp, Map, class instances, etc.).
|
|
385
|
+
// If the object opts in to a JSON representation via toJSON (e.g. Date),
|
|
386
|
+
// honour it — recurse on the returned value. Otherwise dump own enumerable
|
|
387
|
+
// string-keyed properties; this is faster than the JSON round-trip and
|
|
388
|
+
// preserves `undefined` values nested inside class instances.
|
|
389
|
+
JSValue j_toJSON = JS_GetPropertyStr(ctx, j_val, "toJSON");
|
|
390
|
+
if (JS_IsFunction(ctx, j_toJSON))
|
|
364
391
|
{
|
|
392
|
+
JSValue j_jsonValue = JS_Call(ctx, j_toJSON, j_val, 0, NULL);
|
|
393
|
+
JS_FreeValue(ctx, j_toJSON);
|
|
394
|
+
VALUE r_result = to_rb_value_inner(ctx, j_jsonValue, r_visited);
|
|
395
|
+
JS_FreeValue(ctx, j_jsonValue);
|
|
365
396
|
return r_result;
|
|
366
397
|
}
|
|
398
|
+
JS_FreeValue(ctx, j_toJSON);
|
|
399
|
+
return js_plain_object_to_rb(ctx, j_val, r_visited);
|
|
367
400
|
}
|
|
368
401
|
case JS_TAG_NULL:
|
|
369
402
|
return Qnil;
|
|
@@ -419,6 +452,14 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
|
|
|
419
452
|
{
|
|
420
453
|
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_INTERRUPTED_ERROR);
|
|
421
454
|
}
|
|
455
|
+
else if (strcmp(errorClassName, "InternalError") == 0 && strstr(errorClassMessage, "out of memory") != NULL)
|
|
456
|
+
{
|
|
457
|
+
// Once OOM has fired, the QuickJS heap is in a state where another
|
|
458
|
+
// throw inside the parser-error path can corrupt the shape table and
|
|
459
|
+
// segfault. Mark the VM so further eval/call calls refuse cleanly.
|
|
460
|
+
data->oom_poisoned = true;
|
|
461
|
+
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
|
|
462
|
+
}
|
|
422
463
|
else
|
|
423
464
|
{
|
|
424
465
|
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
|
|
@@ -755,6 +796,40 @@ static JSValue js_console_error(JSContext *ctx, JSValueConst this, int argc, JSV
|
|
|
755
796
|
return js_quickjsrb_log(ctx, this, argc, argv, "error");
|
|
756
797
|
}
|
|
757
798
|
|
|
799
|
+
// Polyfill bytecode load + eval is the heavy part of VM construction
|
|
800
|
+
// (POLYFILL_INTL alone is ~140ms — FormatJS locale data + IANA TZ tables).
|
|
801
|
+
// Run it without the GVL so a background warmer thread can populate a VM
|
|
802
|
+
// pool in parallel with the main thread on multi-core hosts.
|
|
803
|
+
//
|
|
804
|
+
// Releasing the GVL here is only safe because the polyfills are pure JS
|
|
805
|
+
// (FormatJS / file / encoding / URL bundles) and no Ruby-bridged callbacks
|
|
806
|
+
// have been registered on globalThis yet at this point in vm_m_initialize.
|
|
807
|
+
// If the order ever changes — e.g. moving define_function setup ahead of
|
|
808
|
+
// the polyfill loads — the polyfill bytecode could re-enter Ruby without
|
|
809
|
+
// the GVL held. Keep host callback registration after polyfill loading.
|
|
810
|
+
struct polyfill_load_args
|
|
811
|
+
{
|
|
812
|
+
JSContext *ctx;
|
|
813
|
+
const uint8_t *buf;
|
|
814
|
+
size_t buf_len;
|
|
815
|
+
JSValue result;
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
static void *polyfill_load_no_gvl(void *p)
|
|
819
|
+
{
|
|
820
|
+
struct polyfill_load_args *args = p;
|
|
821
|
+
JSValue obj = JS_ReadObject(args->ctx, args->buf, args->buf_len, JS_READ_OBJ_BYTECODE);
|
|
822
|
+
args->result = JS_EvalFunction(args->ctx, obj); // frees obj
|
|
823
|
+
return NULL;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
static JSValue load_polyfill_bytecode(JSContext *ctx, const uint8_t *buf, size_t buf_len)
|
|
827
|
+
{
|
|
828
|
+
struct polyfill_load_args args = {ctx, buf, buf_len, JS_UNDEFINED};
|
|
829
|
+
rb_thread_call_without_gvl(polyfill_load_no_gvl, &args, NULL, NULL);
|
|
830
|
+
return args.result;
|
|
831
|
+
}
|
|
832
|
+
|
|
758
833
|
static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
759
834
|
{
|
|
760
835
|
VALUE r_opts;
|
|
@@ -820,15 +895,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
820
895
|
JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), "<vm>", JS_EVAL_TYPE_GLOBAL);
|
|
821
896
|
JS_FreeValue(data->context, j_defineIntl);
|
|
822
897
|
|
|
823
|
-
JSValue
|
|
824
|
-
JSValue j_polyfillIntlResult = JS_EvalFunction(data->context, j_polyfillIntlObject); // Frees polyfillIntlObject
|
|
898
|
+
JSValue j_polyfillIntlResult = load_polyfill_bytecode(data->context, &qjsc_polyfill_intl_en_min, qjsc_polyfill_intl_en_min_size);
|
|
825
899
|
JS_FreeValue(data->context, j_polyfillIntlResult);
|
|
826
900
|
}
|
|
827
901
|
|
|
828
902
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillFileId))))
|
|
829
903
|
{
|
|
830
|
-
JSValue
|
|
831
|
-
JSValue j_polyfillFileResult = JS_EvalFunction(data->context, j_polyfillFileObject);
|
|
904
|
+
JSValue j_polyfillFileResult = load_polyfill_bytecode(data->context, &qjsc_polyfill_file_min, qjsc_polyfill_file_min_size);
|
|
832
905
|
JS_FreeValue(data->context, j_polyfillFileResult);
|
|
833
906
|
|
|
834
907
|
quickjsrb_init_file_proxy(data);
|
|
@@ -836,15 +909,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
836
909
|
|
|
837
910
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillEncodingId))))
|
|
838
911
|
{
|
|
839
|
-
JSValue
|
|
840
|
-
JSValue j_polyfillEncodingResult = JS_EvalFunction(data->context, j_polyfillEncodingObject);
|
|
912
|
+
JSValue j_polyfillEncodingResult = load_polyfill_bytecode(data->context, &qjsc_polyfill_encoding_min, qjsc_polyfill_encoding_min_size);
|
|
841
913
|
JS_FreeValue(data->context, j_polyfillEncodingResult);
|
|
842
914
|
}
|
|
843
915
|
|
|
844
916
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillUrlId))))
|
|
845
917
|
{
|
|
846
|
-
JSValue
|
|
847
|
-
JSValue j_polyfillUrlResult = JS_EvalFunction(data->context, j_polyfillUrlObject);
|
|
918
|
+
JSValue j_polyfillUrlResult = load_polyfill_bytecode(data->context, &qjsc_polyfill_url_min, qjsc_polyfill_url_min_size);
|
|
848
919
|
JS_FreeValue(data->context, j_polyfillUrlResult);
|
|
849
920
|
}
|
|
850
921
|
|
|
@@ -853,6 +924,10 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
853
924
|
quickjsrb_init_crypto(data->context, j_global);
|
|
854
925
|
}
|
|
855
926
|
|
|
927
|
+
// Host callbacks (console, setTimeout, Ruby-bridged functions) are
|
|
928
|
+
// registered below this point — after all polyfill loading above.
|
|
929
|
+
// load_polyfill_bytecode releases the GVL; any code moved above this
|
|
930
|
+
// line that touches Ruby APIs must re-acquire it first.
|
|
856
931
|
JSValue j_console = JS_NewObject(data->context);
|
|
857
932
|
JS_SetPropertyStr(
|
|
858
933
|
data->context, j_console, "log",
|
|
@@ -900,35 +975,69 @@ static VALUE to_rb_return_value(JSContext *ctx, JSValue j_val)
|
|
|
900
975
|
return result;
|
|
901
976
|
}
|
|
902
977
|
|
|
903
|
-
static
|
|
978
|
+
static void check_oom_poisoned(VMData *data)
|
|
904
979
|
{
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
980
|
+
if (data->oom_poisoned)
|
|
981
|
+
{
|
|
982
|
+
VALUE r_msg = rb_str_new2("VM is poisoned: a previous evaluation hit out-of-memory; further evaluation may segfault. Recreate the Quickjs::VM.");
|
|
983
|
+
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_msg, Qnil));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
910
986
|
|
|
987
|
+
// Validate that r_code is a String and resolve the :filename option (default "<code>")
|
|
988
|
+
// from the keyword-args hash that rb_scan_args(... "1:") collects. Both eval_code and
|
|
989
|
+
// compile share this argument shape.
|
|
990
|
+
static const char *parse_code_and_filename(VALUE r_code, VALUE r_opts)
|
|
991
|
+
{
|
|
911
992
|
if (!RB_TYPE_P(r_code, T_STRING))
|
|
912
993
|
{
|
|
913
994
|
VALUE r_code_class = rb_class_name(CLASS_OF(r_code));
|
|
914
995
|
rb_raise(rb_eTypeError, "JavaScript code must be a String, got %s", StringValueCStr(r_code_class));
|
|
915
996
|
}
|
|
997
|
+
if (NIL_P(r_opts))
|
|
998
|
+
return "<code>";
|
|
999
|
+
VALUE r_filename = rb_hash_aref(r_opts, ID2SYM(rb_intern("filename")));
|
|
1000
|
+
if (NIL_P(r_filename))
|
|
1001
|
+
return "<code>";
|
|
1002
|
+
Check_Type(r_filename, T_STRING);
|
|
1003
|
+
return StringValueCStr(r_filename);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
static void arm_eval_timer(VMData *data)
|
|
1007
|
+
{
|
|
1008
|
+
clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
|
|
1009
|
+
JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
|
|
1013
|
+
{
|
|
1014
|
+
VMData *data;
|
|
1015
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1016
|
+
|
|
1017
|
+
check_oom_poisoned(data);
|
|
1018
|
+
|
|
1019
|
+
VALUE r_code, r_opts;
|
|
1020
|
+
rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
|
|
1021
|
+
const char *filename = parse_code_and_filename(r_code, r_opts);
|
|
916
1022
|
|
|
917
|
-
|
|
1023
|
+
bool async_mode = true;
|
|
918
1024
|
if (!NIL_P(r_opts))
|
|
919
1025
|
{
|
|
920
|
-
VALUE
|
|
921
|
-
if (
|
|
922
|
-
|
|
923
|
-
Check_Type(r_filename, T_STRING);
|
|
924
|
-
filename = StringValueCStr(r_filename);
|
|
925
|
-
}
|
|
1026
|
+
VALUE r_async = rb_hash_aref(r_opts, ID2SYM(rb_intern("async")));
|
|
1027
|
+
if (r_async == Qfalse)
|
|
1028
|
+
async_mode = false;
|
|
926
1029
|
}
|
|
927
1030
|
|
|
928
|
-
|
|
929
|
-
JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
|
|
1031
|
+
arm_eval_timer(data);
|
|
930
1032
|
|
|
931
1033
|
StringValue(r_code);
|
|
1034
|
+
|
|
1035
|
+
if (!async_mode)
|
|
1036
|
+
{
|
|
1037
|
+
JSValue j_codeResult = JS_Eval(data->context, RSTRING_PTR(r_code), RSTRING_LEN(r_code), filename, JS_EVAL_TYPE_GLOBAL);
|
|
1038
|
+
return to_rb_return_value(data->context, j_codeResult);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
932
1041
|
JSValue j_codeResult = JS_Eval(data->context, RSTRING_PTR(r_code), RSTRING_LEN(r_code), filename, JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC);
|
|
933
1042
|
JSValue j_awaitedResult = js_std_await(data->context, j_codeResult); // This frees j_codeResult
|
|
934
1043
|
// JS_EVAL_FLAG_ASYNC wraps the result in {value, done} — extract the actual value
|
|
@@ -938,6 +1047,74 @@ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
|
|
|
938
1047
|
return to_rb_return_value(data->context, j_returnedValue);
|
|
939
1048
|
}
|
|
940
1049
|
|
|
1050
|
+
static VALUE vm_m_compile(int argc, VALUE *argv, VALUE r_self)
|
|
1051
|
+
{
|
|
1052
|
+
VMData *data;
|
|
1053
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1054
|
+
|
|
1055
|
+
VALUE r_code, r_opts;
|
|
1056
|
+
rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
|
|
1057
|
+
const char *filename = parse_code_and_filename(r_code, r_opts);
|
|
1058
|
+
|
|
1059
|
+
arm_eval_timer(data);
|
|
1060
|
+
|
|
1061
|
+
StringValue(r_code);
|
|
1062
|
+
JSValue j_func = JS_Eval(data->context, RSTRING_PTR(r_code), RSTRING_LEN(r_code), filename,
|
|
1063
|
+
JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
1064
|
+
if (JS_IsException(j_func))
|
|
1065
|
+
{
|
|
1066
|
+
return to_rb_value(data->context, j_func); // raises Ruby exception
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
size_t out_len;
|
|
1070
|
+
uint8_t *out_buf = JS_WriteObject(data->context, &out_len, j_func, JS_WRITE_OBJ_BYTECODE);
|
|
1071
|
+
JS_FreeValue(data->context, j_func);
|
|
1072
|
+
if (out_buf == NULL)
|
|
1073
|
+
{
|
|
1074
|
+
VALUE r_msg = rb_str_new2("failed to serialize compiled bytecode");
|
|
1075
|
+
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_msg, Qnil));
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
VALUE r_bytecode = rb_str_new((const char *)out_buf, (long)out_len);
|
|
1079
|
+
rb_enc_associate(r_bytecode, rb_ascii8bit_encoding());
|
|
1080
|
+
js_free(data->context, out_buf);
|
|
1081
|
+
return rb_obj_freeze(r_bytecode);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
1085
|
+
{
|
|
1086
|
+
VMData *data;
|
|
1087
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1088
|
+
|
|
1089
|
+
if (!RB_TYPE_P(r_bytecode, T_STRING))
|
|
1090
|
+
{
|
|
1091
|
+
VALUE r_class = rb_class_name(CLASS_OF(r_bytecode));
|
|
1092
|
+
rb_raise(rb_eTypeError, "Bytecode must be a String, got %s", StringValueCStr(r_class));
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
StringValue(r_bytecode);
|
|
1096
|
+
|
|
1097
|
+
arm_eval_timer(data);
|
|
1098
|
+
|
|
1099
|
+
// GVL intentionally held here: user bytecode may invoke Ruby-bridged
|
|
1100
|
+
// callbacks registered via define_function, which call Ruby APIs.
|
|
1101
|
+
// Unlike polyfill_load_no_gvl, this path cannot release the GVL safely.
|
|
1102
|
+
JSValue j_func = JS_ReadObject(data->context,
|
|
1103
|
+
(const uint8_t *)RSTRING_PTR(r_bytecode),
|
|
1104
|
+
(size_t)RSTRING_LEN(r_bytecode),
|
|
1105
|
+
JS_READ_OBJ_BYTECODE);
|
|
1106
|
+
if (JS_IsException(j_func))
|
|
1107
|
+
{
|
|
1108
|
+
return to_rb_value(data->context, j_func); // raises
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
JSValue j_codeResult = JS_EvalFunction(data->context, j_func); // frees j_func
|
|
1112
|
+
JSValue j_awaitedResult = js_std_await(data->context, j_codeResult);
|
|
1113
|
+
JSValue j_returnedValue = JS_GetPropertyStr(data->context, j_awaitedResult, "value");
|
|
1114
|
+
JS_FreeValue(data->context, j_awaitedResult);
|
|
1115
|
+
return to_rb_return_value(data->context, j_returnedValue);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
941
1118
|
static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
942
1119
|
{
|
|
943
1120
|
rb_need_block();
|
|
@@ -1070,6 +1247,8 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1070
1247
|
VMData *data;
|
|
1071
1248
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1072
1249
|
|
|
1250
|
+
check_oom_poisoned(data);
|
|
1251
|
+
|
|
1073
1252
|
JSValue j_this = JS_UNDEFINED;
|
|
1074
1253
|
JSValue j_func;
|
|
1075
1254
|
|
|
@@ -1297,11 +1476,53 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
|
|
|
1297
1476
|
rb_define_alloc_func(r_class_vm, vm_alloc);
|
|
1298
1477
|
rb_define_method(r_class_vm, "initialize", vm_m_initialize, -1);
|
|
1299
1478
|
rb_define_method(r_class_vm, "eval_code", vm_m_evalCode, -1);
|
|
1479
|
+
rb_define_private_method(r_class_vm, "_compile_to_bytecode", vm_m_compile, -1);
|
|
1480
|
+
rb_define_private_method(r_class_vm, "_run_bytecode", vm_m_evalBytecode, 1);
|
|
1300
1481
|
rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
|
|
1301
1482
|
rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
|
|
1302
1483
|
rb_define_method(r_class_vm, "import", vm_m_import, -1);
|
|
1303
1484
|
rb_define_method(r_class_vm, "module_loader", vm_m_get_module_loader, 0);
|
|
1304
1485
|
rb_define_method(r_class_vm, "module_loader=", vm_m_set_module_loader, 1);
|
|
1305
1486
|
rb_define_method(r_class_vm, "on_log", vm_m_on_log, 0);
|
|
1487
|
+
rb_define_method(r_class_vm, "memory_usage", vm_m_memoryUsage, 0);
|
|
1488
|
+
rb_define_method(r_class_vm, "gc!", vm_m_runGC, 0);
|
|
1489
|
+
rb_define_method(r_class_vm, "memory_poisoned?", vm_m_memoryPoisoned, 0);
|
|
1306
1490
|
r_define_log_class(r_class_vm);
|
|
1307
1491
|
}
|
|
1492
|
+
|
|
1493
|
+
static VALUE vm_m_memoryUsage(VALUE r_self)
|
|
1494
|
+
{
|
|
1495
|
+
VMData *data;
|
|
1496
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1497
|
+
JSMemoryUsage s;
|
|
1498
|
+
JS_ComputeMemoryUsage(JS_GetRuntime(data->context), &s);
|
|
1499
|
+
VALUE h = rb_hash_new();
|
|
1500
|
+
rb_hash_aset(h, ID2SYM(rb_intern("malloc_size")), LL2NUM(s.malloc_size));
|
|
1501
|
+
rb_hash_aset(h, ID2SYM(rb_intern("malloc_limit")), LL2NUM(s.malloc_limit));
|
|
1502
|
+
rb_hash_aset(h, ID2SYM(rb_intern("memory_used_size")), LL2NUM(s.memory_used_size));
|
|
1503
|
+
rb_hash_aset(h, ID2SYM(rb_intern("atom_count")), LL2NUM(s.atom_count));
|
|
1504
|
+
rb_hash_aset(h, ID2SYM(rb_intern("str_count")), LL2NUM(s.str_count));
|
|
1505
|
+
rb_hash_aset(h, ID2SYM(rb_intern("obj_count")), LL2NUM(s.obj_count));
|
|
1506
|
+
rb_hash_aset(h, ID2SYM(rb_intern("prop_count")), LL2NUM(s.prop_count));
|
|
1507
|
+
rb_hash_aset(h, ID2SYM(rb_intern("shape_count")), LL2NUM(s.shape_count));
|
|
1508
|
+
rb_hash_aset(h, ID2SYM(rb_intern("js_func_count")), LL2NUM(s.js_func_count));
|
|
1509
|
+
rb_hash_aset(h, ID2SYM(rb_intern("js_func_code_size")), LL2NUM(s.js_func_code_size));
|
|
1510
|
+
rb_hash_aset(h, ID2SYM(rb_intern("c_func_count")), LL2NUM(s.c_func_count));
|
|
1511
|
+
rb_hash_aset(h, ID2SYM(rb_intern("array_count")), LL2NUM(s.array_count));
|
|
1512
|
+
return h;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
static VALUE vm_m_runGC(VALUE r_self)
|
|
1516
|
+
{
|
|
1517
|
+
VMData *data;
|
|
1518
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1519
|
+
JS_RunGC(JS_GetRuntime(data->context));
|
|
1520
|
+
return Qnil;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
static VALUE vm_m_memoryPoisoned(VALUE r_self)
|
|
1524
|
+
{
|
|
1525
|
+
VMData *data;
|
|
1526
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1527
|
+
return data->oom_poisoned ? Qtrue : Qfalse;
|
|
1528
|
+
}
|
data/ext/quickjsrb/quickjsrb.h
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
#define QUICKJSRB_H 1
|
|
3
3
|
|
|
4
4
|
#include "ruby.h"
|
|
5
|
+
#include "ruby/encoding.h"
|
|
6
|
+
#include "ruby/thread.h"
|
|
5
7
|
|
|
6
8
|
#include "quickjs.h"
|
|
7
9
|
#include "quickjs-libc.h"
|
|
@@ -56,6 +58,12 @@ typedef struct VMData
|
|
|
56
58
|
VALUE alive_objects;
|
|
57
59
|
VALUE module_loader;
|
|
58
60
|
JSValue j_file_proxy_creator;
|
|
61
|
+
// Once the runtime has hit JS-level "out of memory", the QuickJS heap is in
|
|
62
|
+
// a fragile state where further evaluation can trigger a use-after-free in
|
|
63
|
+
// the parser-error-during-OOM cascade (segfault inside js_shape_hash_unlink).
|
|
64
|
+
// Trip this flag so subsequent eval_code/call calls refuse cleanly with a
|
|
65
|
+
// Ruby exception instead of risking a process crash.
|
|
66
|
+
bool oom_poisoned;
|
|
59
67
|
} VMData;
|
|
60
68
|
|
|
61
69
|
static void vm_free(void *ptr)
|
|
@@ -109,6 +117,18 @@ static const rb_data_type_t vm_type = {
|
|
|
109
117
|
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
110
118
|
};
|
|
111
119
|
|
|
120
|
+
struct vm_create_args
|
|
121
|
+
{
|
|
122
|
+
JSContext *context;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
static void *vm_create_no_gvl(void *p)
|
|
126
|
+
{
|
|
127
|
+
struct vm_create_args *args = p;
|
|
128
|
+
args->context = JS_NewContext(JS_NewRuntime());
|
|
129
|
+
return NULL;
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
static VALUE vm_alloc(VALUE r_self)
|
|
113
133
|
{
|
|
114
134
|
VMData *data;
|
|
@@ -118,12 +138,17 @@ static VALUE vm_alloc(VALUE r_self)
|
|
|
118
138
|
data->alive_objects = rb_hash_new();
|
|
119
139
|
data->module_loader = Qnil;
|
|
120
140
|
data->j_file_proxy_creator = JS_UNDEFINED;
|
|
141
|
+
data->oom_poisoned = false;
|
|
121
142
|
|
|
122
143
|
EvalTime *eval_time = malloc(sizeof(EvalTime));
|
|
123
144
|
data->eval_time = eval_time;
|
|
124
145
|
|
|
125
|
-
JSRuntime
|
|
126
|
-
|
|
146
|
+
// JSRuntime / JSContext creation is pure QuickJS C work — no Ruby state
|
|
147
|
+
// touched. Release the GVL so a background warmer thread can run this in
|
|
148
|
+
// parallel with the main thread on multi-core hosts.
|
|
149
|
+
struct vm_create_args args = {NULL};
|
|
150
|
+
rb_thread_call_without_gvl(vm_create_no_gvl, &args, NULL, NULL);
|
|
151
|
+
data->context = args.context;
|
|
127
152
|
|
|
128
153
|
return obj;
|
|
129
154
|
}
|