quickjs 0.15.1 → 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/CLAUDE.md +8 -0
- data/README.md +66 -0
- data/ext/quickjsrb/quickjsrb.c +361 -57
- data/ext/quickjsrb/quickjsrb.h +31 -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 +22 -1
- metadata +2 -1
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);
|
|
@@ -470,6 +511,59 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
|
|
|
470
511
|
}
|
|
471
512
|
}
|
|
472
513
|
|
|
514
|
+
struct module_loader_call_args
|
|
515
|
+
{
|
|
516
|
+
VALUE proc;
|
|
517
|
+
VALUE r_module_name;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
static VALUE r_module_loader_call(VALUE r_args_val)
|
|
521
|
+
{
|
|
522
|
+
struct module_loader_call_args *args = (struct module_loader_call_args *)r_args_val;
|
|
523
|
+
return rb_funcall(args->proc, rb_intern("call"), 1, args->r_module_name);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_name, void *opaque, JSValueConst attributes)
|
|
527
|
+
{
|
|
528
|
+
VMData *data = JS_GetContextOpaque(ctx);
|
|
529
|
+
if (NIL_P(data->module_loader))
|
|
530
|
+
return js_module_loader(ctx, module_name, opaque, attributes);
|
|
531
|
+
|
|
532
|
+
struct module_loader_call_args args = {data->module_loader, rb_str_new_cstr(module_name)};
|
|
533
|
+
int state;
|
|
534
|
+
VALUE r_source = rb_protect(r_module_loader_call, (VALUE)&args, &state);
|
|
535
|
+
if (state)
|
|
536
|
+
{
|
|
537
|
+
VALUE r_error = rb_errinfo();
|
|
538
|
+
rb_set_errinfo(Qnil);
|
|
539
|
+
JSValue j_error = j_error_from_ruby_error(ctx, r_error);
|
|
540
|
+
JS_Throw(ctx, j_error);
|
|
541
|
+
return NULL;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (NIL_P(r_source) || r_source == Qfalse)
|
|
545
|
+
{
|
|
546
|
+
JS_ThrowReferenceError(ctx, "module loader returned no source for '%s'", module_name);
|
|
547
|
+
return NULL;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!RB_TYPE_P(r_source, T_STRING))
|
|
551
|
+
{
|
|
552
|
+
JS_ThrowTypeError(ctx, "module loader must return a String or nil, got %s", rb_obj_classname(r_source));
|
|
553
|
+
return NULL;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
JSValue j_func = JS_Eval(ctx, RSTRING_PTR(r_source), RSTRING_LEN(r_source), module_name,
|
|
557
|
+
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
558
|
+
if (JS_IsException(j_func))
|
|
559
|
+
return NULL;
|
|
560
|
+
|
|
561
|
+
js_module_set_import_meta(ctx, j_func, FALSE, FALSE);
|
|
562
|
+
JSModuleDef *m = JS_VALUE_GET_PTR(j_func);
|
|
563
|
+
JS_FreeValue(ctx, j_func);
|
|
564
|
+
return m;
|
|
565
|
+
}
|
|
566
|
+
|
|
473
567
|
static VALUE r_try_call_proc(VALUE r_try_args)
|
|
474
568
|
{
|
|
475
569
|
return rb_funcall(
|
|
@@ -702,6 +796,40 @@ static JSValue js_console_error(JSContext *ctx, JSValueConst this, int argc, JSV
|
|
|
702
796
|
return js_quickjsrb_log(ctx, this, argc, argv, "error");
|
|
703
797
|
}
|
|
704
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
|
+
|
|
705
833
|
static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
706
834
|
{
|
|
707
835
|
VALUE r_opts;
|
|
@@ -732,7 +860,7 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
732
860
|
JS_SetMemoryLimit(runtime, NUM2UINT(r_memory_limit));
|
|
733
861
|
JS_SetMaxStackSize(runtime, NUM2UINT(r_max_stack_size));
|
|
734
862
|
|
|
735
|
-
JS_SetModuleLoaderFunc2(runtime, NULL,
|
|
863
|
+
JS_SetModuleLoaderFunc2(runtime, NULL, quickjsrb_module_loader, js_module_check_attributes, NULL);
|
|
736
864
|
js_std_init_handlers(runtime);
|
|
737
865
|
|
|
738
866
|
JSValue j_global = JS_GetGlobalObject(data->context);
|
|
@@ -767,15 +895,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
767
895
|
JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), "<vm>", JS_EVAL_TYPE_GLOBAL);
|
|
768
896
|
JS_FreeValue(data->context, j_defineIntl);
|
|
769
897
|
|
|
770
|
-
JSValue
|
|
771
|
-
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);
|
|
772
899
|
JS_FreeValue(data->context, j_polyfillIntlResult);
|
|
773
900
|
}
|
|
774
901
|
|
|
775
902
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillFileId))))
|
|
776
903
|
{
|
|
777
|
-
JSValue
|
|
778
|
-
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);
|
|
779
905
|
JS_FreeValue(data->context, j_polyfillFileResult);
|
|
780
906
|
|
|
781
907
|
quickjsrb_init_file_proxy(data);
|
|
@@ -783,15 +909,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
783
909
|
|
|
784
910
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillEncodingId))))
|
|
785
911
|
{
|
|
786
|
-
JSValue
|
|
787
|
-
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);
|
|
788
913
|
JS_FreeValue(data->context, j_polyfillEncodingResult);
|
|
789
914
|
}
|
|
790
915
|
|
|
791
916
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillUrlId))))
|
|
792
917
|
{
|
|
793
|
-
JSValue
|
|
794
|
-
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);
|
|
795
919
|
JS_FreeValue(data->context, j_polyfillUrlResult);
|
|
796
920
|
}
|
|
797
921
|
|
|
@@ -800,6 +924,10 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
800
924
|
quickjsrb_init_crypto(data->context, j_global);
|
|
801
925
|
}
|
|
802
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.
|
|
803
931
|
JSValue j_console = JS_NewObject(data->context);
|
|
804
932
|
JS_SetPropertyStr(
|
|
805
933
|
data->context, j_console, "log",
|
|
@@ -847,35 +975,69 @@ static VALUE to_rb_return_value(JSContext *ctx, JSValue j_val)
|
|
|
847
975
|
return result;
|
|
848
976
|
}
|
|
849
977
|
|
|
850
|
-
static
|
|
978
|
+
static void check_oom_poisoned(VMData *data)
|
|
851
979
|
{
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
+
}
|
|
857
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
|
+
{
|
|
858
992
|
if (!RB_TYPE_P(r_code, T_STRING))
|
|
859
993
|
{
|
|
860
994
|
VALUE r_code_class = rb_class_name(CLASS_OF(r_code));
|
|
861
995
|
rb_raise(rb_eTypeError, "JavaScript code must be a String, got %s", StringValueCStr(r_code_class));
|
|
862
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);
|
|
863
1018
|
|
|
864
|
-
|
|
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);
|
|
1022
|
+
|
|
1023
|
+
bool async_mode = true;
|
|
865
1024
|
if (!NIL_P(r_opts))
|
|
866
1025
|
{
|
|
867
|
-
VALUE
|
|
868
|
-
if (
|
|
869
|
-
|
|
870
|
-
Check_Type(r_filename, T_STRING);
|
|
871
|
-
filename = StringValueCStr(r_filename);
|
|
872
|
-
}
|
|
1026
|
+
VALUE r_async = rb_hash_aref(r_opts, ID2SYM(rb_intern("async")));
|
|
1027
|
+
if (r_async == Qfalse)
|
|
1028
|
+
async_mode = false;
|
|
873
1029
|
}
|
|
874
1030
|
|
|
875
|
-
|
|
876
|
-
JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
|
|
1031
|
+
arm_eval_timer(data);
|
|
877
1032
|
|
|
878
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
|
+
|
|
879
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);
|
|
880
1042
|
JSValue j_awaitedResult = js_std_await(data->context, j_codeResult); // This frees j_codeResult
|
|
881
1043
|
// JS_EVAL_FLAG_ASYNC wraps the result in {value, done} — extract the actual value
|
|
@@ -885,6 +1047,74 @@ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
|
|
|
885
1047
|
return to_rb_return_value(data->context, j_returnedValue);
|
|
886
1048
|
}
|
|
887
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
|
+
|
|
888
1118
|
static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
889
1119
|
{
|
|
890
1120
|
rb_need_block();
|
|
@@ -1017,6 +1247,8 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1017
1247
|
VMData *data;
|
|
1018
1248
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1019
1249
|
|
|
1250
|
+
check_oom_poisoned(data);
|
|
1251
|
+
|
|
1020
1252
|
JSValue j_this = JS_UNDEFINED;
|
|
1021
1253
|
JSValue j_func;
|
|
1022
1254
|
|
|
@@ -1143,6 +1375,25 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1143
1375
|
return to_rb_return_value(data->context, js_std_await(data->context, j_result));
|
|
1144
1376
|
}
|
|
1145
1377
|
|
|
1378
|
+
static VALUE vm_m_set_module_loader(VALUE r_self, VALUE r_loader)
|
|
1379
|
+
{
|
|
1380
|
+
VMData *data;
|
|
1381
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1382
|
+
|
|
1383
|
+
if (!NIL_P(r_loader) && !rb_obj_is_kind_of(r_loader, rb_cProc))
|
|
1384
|
+
rb_raise(rb_eTypeError, "module_loader must be a Proc or nil");
|
|
1385
|
+
|
|
1386
|
+
data->module_loader = r_loader;
|
|
1387
|
+
return r_loader;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
static VALUE vm_m_get_module_loader(VALUE r_self)
|
|
1391
|
+
{
|
|
1392
|
+
VMData *data;
|
|
1393
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1394
|
+
return data->module_loader;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1146
1397
|
static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
1147
1398
|
{
|
|
1148
1399
|
VALUE r_import_string, r_opts;
|
|
@@ -1150,7 +1401,8 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1150
1401
|
if (NIL_P(r_opts))
|
|
1151
1402
|
r_opts = rb_hash_new();
|
|
1152
1403
|
VALUE r_from = rb_hash_aref(r_opts, ID2SYM(rb_intern("from")));
|
|
1153
|
-
|
|
1404
|
+
VALUE r_filename = rb_hash_aref(r_opts, ID2SYM(rb_intern("filename")));
|
|
1405
|
+
if (NIL_P(r_from) && NIL_P(r_filename))
|
|
1154
1406
|
{
|
|
1155
1407
|
VALUE r_error_message = rb_str_new2("missing import source");
|
|
1156
1408
|
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
|
|
@@ -1161,16 +1413,24 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1161
1413
|
VMData *data;
|
|
1162
1414
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1163
1415
|
|
|
1164
|
-
char *filename
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1416
|
+
char *filename;
|
|
1417
|
+
if (!NIL_P(r_filename))
|
|
1418
|
+
{
|
|
1419
|
+
filename = StringValueCStr(r_filename);
|
|
1420
|
+
}
|
|
1421
|
+
else
|
|
1168
1422
|
{
|
|
1423
|
+
filename = random_string();
|
|
1424
|
+
char *source = StringValueCStr(r_from);
|
|
1425
|
+
JSValue module = JS_Eval(data->context, source, strlen(source), filename, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
1426
|
+
if (JS_IsException(module))
|
|
1427
|
+
{
|
|
1428
|
+
JS_FreeValue(data->context, module);
|
|
1429
|
+
return to_rb_value(data->context, module);
|
|
1430
|
+
}
|
|
1431
|
+
js_module_set_import_meta(data->context, module, TRUE, FALSE);
|
|
1169
1432
|
JS_FreeValue(data->context, module);
|
|
1170
|
-
return to_rb_value(data->context, module);
|
|
1171
1433
|
}
|
|
1172
|
-
js_module_set_import_meta(data->context, module, TRUE, FALSE);
|
|
1173
|
-
JS_FreeValue(data->context, module);
|
|
1174
1434
|
|
|
1175
1435
|
VALUE r_import_settings = rb_funcall(
|
|
1176
1436
|
rb_const_get(rb_cClass, rb_intern("Quickjs")),
|
|
@@ -1216,9 +1476,53 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
|
|
|
1216
1476
|
rb_define_alloc_func(r_class_vm, vm_alloc);
|
|
1217
1477
|
rb_define_method(r_class_vm, "initialize", vm_m_initialize, -1);
|
|
1218
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);
|
|
1219
1481
|
rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
|
|
1220
1482
|
rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
|
|
1221
1483
|
rb_define_method(r_class_vm, "import", vm_m_import, -1);
|
|
1484
|
+
rb_define_method(r_class_vm, "module_loader", vm_m_get_module_loader, 0);
|
|
1485
|
+
rb_define_method(r_class_vm, "module_loader=", vm_m_set_module_loader, 1);
|
|
1222
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);
|
|
1223
1490
|
r_define_log_class(r_class_vm);
|
|
1224
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"
|
|
@@ -54,7 +56,14 @@ typedef struct VMData
|
|
|
54
56
|
struct EvalTime *eval_time;
|
|
55
57
|
VALUE log_listener;
|
|
56
58
|
VALUE alive_objects;
|
|
59
|
+
VALUE module_loader;
|
|
57
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;
|
|
58
67
|
} VMData;
|
|
59
68
|
|
|
60
69
|
static void vm_free(void *ptr)
|
|
@@ -85,6 +94,7 @@ static void vm_mark(void *ptr)
|
|
|
85
94
|
rb_gc_mark_movable(data->defined_functions);
|
|
86
95
|
rb_gc_mark_movable(data->log_listener);
|
|
87
96
|
rb_gc_mark_movable(data->alive_objects);
|
|
97
|
+
rb_gc_mark_movable(data->module_loader);
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
static void vm_compact(void *ptr)
|
|
@@ -93,6 +103,7 @@ static void vm_compact(void *ptr)
|
|
|
93
103
|
data->defined_functions = rb_gc_location(data->defined_functions);
|
|
94
104
|
data->log_listener = rb_gc_location(data->log_listener);
|
|
95
105
|
data->alive_objects = rb_gc_location(data->alive_objects);
|
|
106
|
+
data->module_loader = rb_gc_location(data->module_loader);
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
static const rb_data_type_t vm_type = {
|
|
@@ -106,6 +117,18 @@ static const rb_data_type_t vm_type = {
|
|
|
106
117
|
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
107
118
|
};
|
|
108
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
|
+
|
|
109
132
|
static VALUE vm_alloc(VALUE r_self)
|
|
110
133
|
{
|
|
111
134
|
VMData *data;
|
|
@@ -113,13 +136,19 @@ static VALUE vm_alloc(VALUE r_self)
|
|
|
113
136
|
data->defined_functions = rb_hash_new();
|
|
114
137
|
data->log_listener = Qnil;
|
|
115
138
|
data->alive_objects = rb_hash_new();
|
|
139
|
+
data->module_loader = Qnil;
|
|
116
140
|
data->j_file_proxy_creator = JS_UNDEFINED;
|
|
141
|
+
data->oom_poisoned = false;
|
|
117
142
|
|
|
118
143
|
EvalTime *eval_time = malloc(sizeof(EvalTime));
|
|
119
144
|
data->eval_time = eval_time;
|
|
120
145
|
|
|
121
|
-
JSRuntime
|
|
122
|
-
|
|
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;
|
|
123
152
|
|
|
124
153
|
return obj;
|
|
125
154
|
}
|