quickjs 0.17.0 → 0.19.0.pre1
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 +122 -4
- data/ext/quickjsrb/extconf.rb +0 -5
- data/ext/quickjsrb/quickjsrb.c +389 -33
- data/ext/quickjsrb/quickjsrb.h +39 -9
- data/lib/quickjs/polyfills/intl-en.min.js +4 -0
- data/lib/quickjs/polyfills/intl.rb +11 -0
- data/lib/quickjs/polyfills.rb +67 -0
- data/lib/quickjs/version.rb +1 -1
- data/lib/quickjs.rb +23 -5
- data/polyfills/package-lock.json +110 -110
- data/polyfills/package.json +1 -1
- data/polyfills/rolldown.config.mjs +4 -1
- data/sig/quickjs.rbs +16 -2
- metadata +4 -2
- data/ext/quickjsrb/vendor/polyfill-intl-en.min.js +0 -4
data/ext/quickjsrb/quickjsrb.c
CHANGED
|
@@ -13,6 +13,7 @@ const char *featurePolyfillCryptoId = "feature_polyfill_crypto";
|
|
|
13
13
|
|
|
14
14
|
const char *undefinedId = "undefined";
|
|
15
15
|
const char *nanId = "NaN";
|
|
16
|
+
const char *vmInternalFilename = "<vm>";
|
|
16
17
|
|
|
17
18
|
const char *native_errors[] = {
|
|
18
19
|
"SyntaxError",
|
|
@@ -32,6 +33,9 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited);
|
|
|
32
33
|
static VALUE vm_m_memoryUsage(VALUE r_self);
|
|
33
34
|
static VALUE vm_m_runGC(VALUE r_self);
|
|
34
35
|
static VALUE vm_m_memoryPoisoned(VALUE r_self);
|
|
36
|
+
static VALUE vm_m_dispose(VALUE r_self);
|
|
37
|
+
static VALUE vm_m_disposed(VALUE r_self);
|
|
38
|
+
static VALUE vm_m_drainJobs(VALUE r_self);
|
|
35
39
|
|
|
36
40
|
JSValue j_error_from_ruby_error(JSContext *ctx, VALUE r_error)
|
|
37
41
|
{
|
|
@@ -187,6 +191,28 @@ VALUE r_try_json_parse(VALUE r_str)
|
|
|
187
191
|
return rb_funcall(rb_const_get(rb_cClass, rb_intern("JSON")), rb_intern("parse"), 1, r_str);
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
// Convert a JS Error.stack string into a Ruby Array suitable for
|
|
195
|
+
// Exception#set_backtrace. Lines are stripped; empty lines (including the
|
|
196
|
+
// trailing newline that QuickJS appends) are dropped. The frames keep
|
|
197
|
+
// QuickJS's native format ("at func (file:line)") — Ruby's backtrace API
|
|
198
|
+
// doesn't enforce a layout, and reshaping into "file:line:in 'method'"
|
|
199
|
+
// would lose information for no real win.
|
|
200
|
+
static VALUE r_backtrace_from_js_stack(const char *stack)
|
|
201
|
+
{
|
|
202
|
+
if (stack == NULL || stack[0] == '\0')
|
|
203
|
+
return Qnil;
|
|
204
|
+
|
|
205
|
+
VALUE r_lines = rb_str_split(rb_str_new_cstr(stack), "\n");
|
|
206
|
+
VALUE r_filtered = rb_ary_new();
|
|
207
|
+
for (long i = 0; i < RARRAY_LEN(r_lines); i++)
|
|
208
|
+
{
|
|
209
|
+
VALUE r_line = rb_funcall(rb_ary_entry(r_lines, i), rb_intern("strip"), 0);
|
|
210
|
+
if (RSTRING_LEN(r_line) > 0)
|
|
211
|
+
rb_ary_push(r_filtered, r_line);
|
|
212
|
+
}
|
|
213
|
+
return RARRAY_LEN(r_filtered) > 0 ? r_filtered : Qnil;
|
|
214
|
+
}
|
|
215
|
+
|
|
190
216
|
VALUE to_r_json(JSContext *ctx, JSValue j_val)
|
|
191
217
|
{
|
|
192
218
|
JSValue j_stringified = JS_JSONStringify(ctx, j_val, JS_UNDEFINED, JS_UNDEFINED);
|
|
@@ -431,14 +457,11 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
|
431
457
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
432
458
|
VALUE r_headline = rb_str_new2(headline);
|
|
433
459
|
dispatch_log(data, "error", rb_ary_new3(1, r_log_body_new(r_headline, r_headline)));
|
|
434
|
-
|
|
435
|
-
JS_FreeValue(ctx, j_errorClassMessage);
|
|
436
|
-
JS_FreeValue(ctx, j_errorClassName);
|
|
437
|
-
JS_FreeValue(ctx, j_stackTrace);
|
|
438
|
-
JS_FreeCString(ctx, stackTrace);
|
|
439
460
|
free(headline);
|
|
440
461
|
|
|
441
462
|
VALUE r_error_class, r_error_message = rb_str_new2(errorClassMessage);
|
|
463
|
+
VALUE r_error_name = rb_str_new2(errorClassName);
|
|
464
|
+
VALUE r_backtrace = r_backtrace_from_js_stack(stackTrace);
|
|
442
465
|
if (is_native_error_name(errorClassName))
|
|
443
466
|
{
|
|
444
467
|
r_error_class = QUICKJSRB_ERROR_FOR(errorClassName);
|
|
@@ -464,11 +487,18 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
|
464
487
|
{
|
|
465
488
|
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
|
|
466
489
|
}
|
|
490
|
+
JS_FreeValue(ctx, j_errorClassMessage);
|
|
491
|
+
JS_FreeValue(ctx, j_errorClassName);
|
|
492
|
+
JS_FreeValue(ctx, j_stackTrace);
|
|
493
|
+
JS_FreeCString(ctx, stackTrace);
|
|
467
494
|
JS_FreeCString(ctx, errorClassName);
|
|
468
495
|
JS_FreeCString(ctx, errorClassMessage);
|
|
469
496
|
JS_FreeValue(ctx, j_exceptionVal);
|
|
470
497
|
|
|
471
|
-
|
|
498
|
+
VALUE r_exc = rb_funcall(r_error_class, rb_intern("new"), 2, r_error_message, r_error_name);
|
|
499
|
+
if (!NIL_P(r_backtrace))
|
|
500
|
+
rb_funcall(r_exc, rb_intern("set_backtrace"), 1, r_backtrace);
|
|
501
|
+
rb_exc_raise(r_exc);
|
|
472
502
|
}
|
|
473
503
|
else // exception without Error object
|
|
474
504
|
{
|
|
@@ -514,24 +544,48 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
|
514
544
|
struct module_loader_call_args
|
|
515
545
|
{
|
|
516
546
|
VALUE proc;
|
|
517
|
-
VALUE
|
|
547
|
+
VALUE r_specifier;
|
|
548
|
+
VALUE r_importer;
|
|
518
549
|
};
|
|
519
550
|
|
|
551
|
+
// Calls the user's loader with one or two args based on its arity. Procs
|
|
552
|
+
// declared with a single positional (`->(name) { ... }`) get the legacy
|
|
553
|
+
// 1-arg form so existing callers keep working; everything else (2-arity
|
|
554
|
+
// lambdas, varargs procs) receives `(specifier, importer)`.
|
|
520
555
|
static VALUE r_module_loader_call(VALUE r_args_val)
|
|
521
556
|
{
|
|
522
557
|
struct module_loader_call_args *args = (struct module_loader_call_args *)r_args_val;
|
|
523
|
-
|
|
558
|
+
int arity = NUM2INT(rb_funcall(args->proc, rb_intern("arity"), 0));
|
|
559
|
+
if (arity == 1)
|
|
560
|
+
return rb_funcall(args->proc, rb_intern("call"), 1, args->r_specifier);
|
|
561
|
+
return rb_funcall(args->proc, rb_intern("call"), 2, args->r_specifier, args->r_importer);
|
|
524
562
|
}
|
|
525
563
|
|
|
526
|
-
|
|
564
|
+
// Normalize hook. Resolves `(specifier, importer)` to a canonical name by
|
|
565
|
+
// consulting the resolution cache or invoking the user's loader Proc.
|
|
566
|
+
// The Proc's return value drives both the canonical name AND the source
|
|
567
|
+
// the load hook will eval:
|
|
568
|
+
// - String → canonical = specifier, source = the string
|
|
569
|
+
// - { code:, as: } → canonical = as, source = code
|
|
570
|
+
// - nil / false → ReferenceError
|
|
571
|
+
// Source is stashed in `module_source_cache` keyed by canonical so the
|
|
572
|
+
// load hook can pick it up. The resolution cache memoizes the call so
|
|
573
|
+
// the Proc fires at most once per `(specifier, importer)` pair.
|
|
574
|
+
static char *quickjsrb_module_normalize(JSContext *ctx, const char *base_name, const char *name, void *opaque)
|
|
527
575
|
{
|
|
528
576
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
529
|
-
if (NIL_P(data->module_loader))
|
|
530
|
-
return js_module_loader(ctx, module_name, opaque, attributes);
|
|
531
577
|
|
|
532
|
-
|
|
578
|
+
VALUE r_specifier = rb_str_new_cstr(name);
|
|
579
|
+
VALUE r_importer = rb_str_new_cstr(base_name);
|
|
580
|
+
VALUE r_key = rb_ary_new3(2, r_specifier, r_importer);
|
|
581
|
+
|
|
582
|
+
VALUE r_cached_canonical = rb_hash_aref(data->module_resolution_cache, r_key);
|
|
583
|
+
if (!NIL_P(r_cached_canonical))
|
|
584
|
+
return js_strdup(ctx, StringValueCStr(r_cached_canonical));
|
|
585
|
+
|
|
586
|
+
struct module_loader_call_args args = {data->module_loader, r_specifier, r_importer};
|
|
533
587
|
int state;
|
|
534
|
-
VALUE
|
|
588
|
+
VALUE r_return = rb_protect(r_module_loader_call, (VALUE)&args, &state);
|
|
535
589
|
if (state)
|
|
536
590
|
{
|
|
537
591
|
VALUE r_error = rb_errinfo();
|
|
@@ -541,17 +595,61 @@ static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_n
|
|
|
541
595
|
return NULL;
|
|
542
596
|
}
|
|
543
597
|
|
|
544
|
-
if (NIL_P(
|
|
598
|
+
if (NIL_P(r_return) || r_return == Qfalse)
|
|
545
599
|
{
|
|
546
|
-
JS_ThrowReferenceError(ctx, "module loader returned no source for '%s'",
|
|
600
|
+
JS_ThrowReferenceError(ctx, "module loader returned no source for '%s'", name);
|
|
547
601
|
return NULL;
|
|
548
602
|
}
|
|
549
603
|
|
|
550
|
-
|
|
604
|
+
VALUE r_canonical, r_source;
|
|
605
|
+
if (RB_TYPE_P(r_return, T_STRING))
|
|
606
|
+
{
|
|
607
|
+
r_canonical = r_specifier;
|
|
608
|
+
r_source = r_return;
|
|
609
|
+
}
|
|
610
|
+
else if (RB_TYPE_P(r_return, T_HASH))
|
|
611
|
+
{
|
|
612
|
+
r_source = rb_hash_aref(r_return, ID2SYM(rb_intern("code")));
|
|
613
|
+
r_canonical = rb_hash_aref(r_return, ID2SYM(rb_intern("as")));
|
|
614
|
+
if (!RB_TYPE_P(r_source, T_STRING))
|
|
615
|
+
{
|
|
616
|
+
JS_ThrowTypeError(ctx, "module loader Hash must include code: (String, the module source)");
|
|
617
|
+
return NULL;
|
|
618
|
+
}
|
|
619
|
+
if (!RB_TYPE_P(r_canonical, T_STRING))
|
|
620
|
+
{
|
|
621
|
+
JS_ThrowTypeError(ctx, "module loader Hash must include as: (String, the canonical module name)");
|
|
622
|
+
return NULL;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else
|
|
626
|
+
{
|
|
627
|
+
JS_ThrowTypeError(ctx, "module loader must return a String, a Hash with code: and as:, or nil; got %s",
|
|
628
|
+
rb_obj_classname(r_return));
|
|
629
|
+
return NULL;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
rb_hash_aset(data->module_source_cache, r_canonical, r_source);
|
|
633
|
+
rb_hash_aset(data->module_resolution_cache, r_key, r_canonical);
|
|
634
|
+
|
|
635
|
+
return js_strdup(ctx, StringValueCStr(r_canonical));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_name, void *opaque, JSValueConst attributes)
|
|
639
|
+
{
|
|
640
|
+
VMData *data = JS_GetContextOpaque(ctx);
|
|
641
|
+
|
|
642
|
+
VALUE r_canonical = rb_str_new_cstr(module_name);
|
|
643
|
+
VALUE r_source = rb_hash_aref(data->module_source_cache, r_canonical);
|
|
644
|
+
if (NIL_P(r_source))
|
|
551
645
|
{
|
|
552
|
-
|
|
646
|
+
// Defensive: normalize populates this on every miss.
|
|
647
|
+
JS_ThrowReferenceError(ctx, "module loader: no cached source for '%s'", module_name);
|
|
553
648
|
return NULL;
|
|
554
649
|
}
|
|
650
|
+
// QuickJS won't call load again for this canonical — its own module cache
|
|
651
|
+
// takes over — so the source is dead weight once we've compiled it.
|
|
652
|
+
rb_hash_delete(data->module_source_cache, r_canonical);
|
|
555
653
|
|
|
556
654
|
JSValue j_func = JS_Eval(ctx, RSTRING_PTR(r_source), RSTRING_LEN(r_source), module_name,
|
|
557
655
|
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
@@ -564,6 +662,95 @@ static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_n
|
|
|
564
662
|
return m;
|
|
565
663
|
}
|
|
566
664
|
|
|
665
|
+
// When no Ruby loader is set we hand resolution back to QuickJS's defaults
|
|
666
|
+
// (URL-style normalize + filesystem load). When a loader is set we own both
|
|
667
|
+
// phases so we can thread (specifier, importer) through and honor `as:`.
|
|
668
|
+
static void register_module_loader_funcs(VMData *data)
|
|
669
|
+
{
|
|
670
|
+
JSRuntime *runtime = JS_GetRuntime(data->context);
|
|
671
|
+
if (NIL_P(data->module_loader))
|
|
672
|
+
JS_SetModuleLoaderFunc2(runtime, NULL, js_module_loader, js_module_check_attributes, NULL);
|
|
673
|
+
else
|
|
674
|
+
JS_SetModuleLoaderFunc2(runtime, quickjsrb_module_normalize, quickjsrb_module_loader, js_module_check_attributes, NULL);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
static VALUE r_exception_from_js_reason(JSContext *ctx, JSValueConst j_reason)
|
|
678
|
+
{
|
|
679
|
+
if (JS_IsError(ctx, j_reason))
|
|
680
|
+
{
|
|
681
|
+
VALUE r_maybe_ruby_error = find_ruby_error(ctx, j_reason);
|
|
682
|
+
if (!NIL_P(r_maybe_ruby_error))
|
|
683
|
+
return r_maybe_ruby_error;
|
|
684
|
+
|
|
685
|
+
JSValue j_name = JS_GetPropertyStr(ctx, j_reason, "name");
|
|
686
|
+
JSValue j_message = JS_GetPropertyStr(ctx, j_reason, "message");
|
|
687
|
+
JSValue j_stack = JS_GetPropertyStr(ctx, j_reason, "stack");
|
|
688
|
+
const char *name = JS_ToCString(ctx, j_name);
|
|
689
|
+
const char *message = JS_ToCString(ctx, j_message);
|
|
690
|
+
const char *stack = JS_ToCString(ctx, j_stack);
|
|
691
|
+
|
|
692
|
+
VALUE r_class = is_native_error_name(name)
|
|
693
|
+
? QUICKJSRB_ERROR_FOR(name)
|
|
694
|
+
: QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
|
|
695
|
+
VALUE r_exc = rb_funcall(r_class, rb_intern("new"), 2,
|
|
696
|
+
rb_str_new2(message), rb_str_new2(name));
|
|
697
|
+
VALUE r_backtrace = r_backtrace_from_js_stack(stack);
|
|
698
|
+
if (!NIL_P(r_backtrace))
|
|
699
|
+
rb_funcall(r_exc, rb_intern("set_backtrace"), 1, r_backtrace);
|
|
700
|
+
|
|
701
|
+
JS_FreeCString(ctx, name);
|
|
702
|
+
JS_FreeCString(ctx, message);
|
|
703
|
+
if (stack)
|
|
704
|
+
JS_FreeCString(ctx, stack);
|
|
705
|
+
JS_FreeValue(ctx, j_name);
|
|
706
|
+
JS_FreeValue(ctx, j_message);
|
|
707
|
+
JS_FreeValue(ctx, j_stack);
|
|
708
|
+
return r_exc;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const char *str = JS_ToCString(ctx, j_reason);
|
|
712
|
+
VALUE r_exc = rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"),
|
|
713
|
+
2, rb_str_new2(str ? str : "(non-stringifiable rejection)"), Qnil);
|
|
714
|
+
if (str)
|
|
715
|
+
JS_FreeCString(ctx, str);
|
|
716
|
+
return r_exc;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
struct rejection_call_args
|
|
720
|
+
{
|
|
721
|
+
VALUE proc;
|
|
722
|
+
VALUE r_reason;
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
static VALUE r_rejection_call(VALUE r_args_val)
|
|
726
|
+
{
|
|
727
|
+
struct rejection_call_args *args = (struct rejection_call_args *)r_args_val;
|
|
728
|
+
return rb_funcall(args->proc, rb_intern("call"), 1, args->r_reason);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
static void quickjsrb_promise_rejection_tracker(
|
|
732
|
+
JSContext *ctx, JSValueConst promise, JSValueConst reason,
|
|
733
|
+
JS_BOOL is_handled, void *opaque)
|
|
734
|
+
{
|
|
735
|
+
if (is_handled)
|
|
736
|
+
return;
|
|
737
|
+
|
|
738
|
+
VMData *data = JS_GetContextOpaque(ctx);
|
|
739
|
+
if (NIL_P(data->on_unhandled_rejection))
|
|
740
|
+
return;
|
|
741
|
+
|
|
742
|
+
VALUE r_reason = r_exception_from_js_reason(ctx, reason);
|
|
743
|
+
struct rejection_call_args args = {data->on_unhandled_rejection, r_reason};
|
|
744
|
+
int state;
|
|
745
|
+
rb_protect(r_rejection_call, (VALUE)&args, &state);
|
|
746
|
+
if (state)
|
|
747
|
+
{
|
|
748
|
+
// Longjmping out of a QuickJS host callback corrupts the runtime, so
|
|
749
|
+
// a raise inside the user's tracker has to be dropped on the floor.
|
|
750
|
+
rb_set_errinfo(Qnil);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
567
754
|
static VALUE r_try_call_proc(VALUE r_try_args)
|
|
568
755
|
{
|
|
569
756
|
return rb_funcall(
|
|
@@ -860,7 +1047,8 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
860
1047
|
JS_SetMemoryLimit(runtime, NUM2UINT(r_memory_limit));
|
|
861
1048
|
JS_SetMaxStackSize(runtime, NUM2UINT(r_max_stack_size));
|
|
862
1049
|
|
|
863
|
-
|
|
1050
|
+
register_module_loader_funcs(data);
|
|
1051
|
+
JS_SetHostPromiseRejectionTracker(runtime, quickjsrb_promise_rejection_tracker, NULL);
|
|
864
1052
|
js_std_init_handlers(runtime);
|
|
865
1053
|
|
|
866
1054
|
JSValue j_global = JS_GetGlobalObject(data->context);
|
|
@@ -870,7 +1058,7 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
870
1058
|
js_init_module_std(data->context, "std");
|
|
871
1059
|
const char *enableStd = "import * as std from 'std';\n"
|
|
872
1060
|
"globalThis.std = std;\n";
|
|
873
|
-
JSValue j_stdEval = JS_Eval(data->context, enableStd, strlen(enableStd),
|
|
1061
|
+
JSValue j_stdEval = JS_Eval(data->context, enableStd, strlen(enableStd), vmInternalFilename, JS_EVAL_TYPE_MODULE);
|
|
874
1062
|
JS_FreeValue(data->context, j_stdEval);
|
|
875
1063
|
}
|
|
876
1064
|
|
|
@@ -879,7 +1067,7 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
879
1067
|
js_init_module_os(data->context, "os");
|
|
880
1068
|
const char *enableOs = "import * as os from 'os';\n"
|
|
881
1069
|
"globalThis.os = os;\n";
|
|
882
|
-
JSValue j_osEval = JS_Eval(data->context, enableOs, strlen(enableOs),
|
|
1070
|
+
JSValue j_osEval = JS_Eval(data->context, enableOs, strlen(enableOs), vmInternalFilename, JS_EVAL_TYPE_MODULE);
|
|
883
1071
|
JS_FreeValue(data->context, j_osEval);
|
|
884
1072
|
}
|
|
885
1073
|
else if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featureTimeoutId))))
|
|
@@ -889,15 +1077,8 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
889
1077
|
JS_NewCFunction(data->context, js_quickjsrb_set_timeout, "setTimeout", 2));
|
|
890
1078
|
}
|
|
891
1079
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const char *defineIntl = "Object.defineProperty(globalThis, 'Intl', { value:{} });\n";
|
|
895
|
-
JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), "<vm>", JS_EVAL_TYPE_GLOBAL);
|
|
896
|
-
JS_FreeValue(data->context, j_defineIntl);
|
|
897
|
-
|
|
898
|
-
JSValue j_polyfillIntlResult = load_polyfill_bytecode(data->context, &qjsc_polyfill_intl_en_min, qjsc_polyfill_intl_en_min_size);
|
|
899
|
-
JS_FreeValue(data->context, j_polyfillIntlResult);
|
|
900
|
-
}
|
|
1080
|
+
// POLYFILL_INTL is registered Ruby-side via Quickjs.register_polyfill and
|
|
1081
|
+
// applied by the wrapper around VM#initialize in lib/quickjs/polyfills.rb.
|
|
901
1082
|
|
|
902
1083
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillFileId))))
|
|
903
1084
|
{
|
|
@@ -984,6 +1165,15 @@ static void check_oom_poisoned(VMData *data)
|
|
|
984
1165
|
}
|
|
985
1166
|
}
|
|
986
1167
|
|
|
1168
|
+
static void check_disposed(VMData *data)
|
|
1169
|
+
{
|
|
1170
|
+
if (data->disposed)
|
|
1171
|
+
{
|
|
1172
|
+
VALUE r_msg = rb_str_new2("VM has been disposed; create a new Quickjs::VM");
|
|
1173
|
+
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_msg, Qnil));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
987
1177
|
// Validate that r_code is a String and resolve the :filename option (default "<code>")
|
|
988
1178
|
// from the keyword-args hash that rb_scan_args(... "1:") collects. Both eval_code and
|
|
989
1179
|
// compile share this argument shape.
|
|
@@ -1014,6 +1204,7 @@ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
|
|
|
1014
1204
|
VMData *data;
|
|
1015
1205
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1016
1206
|
|
|
1207
|
+
check_disposed(data);
|
|
1017
1208
|
check_oom_poisoned(data);
|
|
1018
1209
|
|
|
1019
1210
|
VALUE r_code, r_opts;
|
|
@@ -1052,6 +1243,8 @@ static VALUE vm_m_compile(int argc, VALUE *argv, VALUE r_self)
|
|
|
1052
1243
|
VMData *data;
|
|
1053
1244
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1054
1245
|
|
|
1246
|
+
check_disposed(data);
|
|
1247
|
+
|
|
1055
1248
|
VALUE r_code, r_opts;
|
|
1056
1249
|
rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
|
|
1057
1250
|
const char *filename = parse_code_and_filename(r_code, r_opts);
|
|
@@ -1086,6 +1279,8 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1086
1279
|
VMData *data;
|
|
1087
1280
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1088
1281
|
|
|
1282
|
+
check_disposed(data);
|
|
1283
|
+
|
|
1089
1284
|
if (!RB_TYPE_P(r_bytecode, T_STRING))
|
|
1090
1285
|
{
|
|
1091
1286
|
VALUE r_class = rb_class_name(CLASS_OF(r_bytecode));
|
|
@@ -1115,6 +1310,36 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1115
1310
|
return to_rb_return_value(data->context, j_returnedValue);
|
|
1116
1311
|
}
|
|
1117
1312
|
|
|
1313
|
+
// Loads pre-compiled polyfill bytecode without arming the eval timer.
|
|
1314
|
+
// The user's `timeout_msec` is a budget for *their* code; running our
|
|
1315
|
+
// own polyfill (~140 ms for FormatJS Intl) under that budget would
|
|
1316
|
+
// interrupt the load on tight defaults. Unlike load_polyfill_bytecode
|
|
1317
|
+
// above we hold the GVL through JS_ReadObject + JS_EvalFunction: the
|
|
1318
|
+
// bytecode buffer is a Ruby String, so releasing would let GC compact
|
|
1319
|
+
// the backing storage out from under us. The static-symbol path can
|
|
1320
|
+
// release safely; this path cannot.
|
|
1321
|
+
static VALUE vm_m_loadPolyfillBytecode(VALUE r_self, VALUE r_bytecode)
|
|
1322
|
+
{
|
|
1323
|
+
VMData *data;
|
|
1324
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1325
|
+
|
|
1326
|
+
check_disposed(data);
|
|
1327
|
+
StringValue(r_bytecode);
|
|
1328
|
+
|
|
1329
|
+
JSValue j_func = JS_ReadObject(data->context,
|
|
1330
|
+
(const uint8_t *)RSTRING_PTR(r_bytecode),
|
|
1331
|
+
(size_t)RSTRING_LEN(r_bytecode),
|
|
1332
|
+
JS_READ_OBJ_BYTECODE);
|
|
1333
|
+
if (JS_IsException(j_func))
|
|
1334
|
+
return to_rb_value(data->context, j_func); // raises
|
|
1335
|
+
|
|
1336
|
+
JSValue j_result = JS_EvalFunction(data->context, j_func); // frees j_func
|
|
1337
|
+
if (JS_IsException(j_result))
|
|
1338
|
+
return to_rb_value(data->context, j_result); // raises
|
|
1339
|
+
JS_FreeValue(data->context, j_result);
|
|
1340
|
+
return Qnil;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1118
1343
|
static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
1119
1344
|
{
|
|
1120
1345
|
rb_need_block();
|
|
@@ -1127,6 +1352,8 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1127
1352
|
VMData *data;
|
|
1128
1353
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1129
1354
|
|
|
1355
|
+
check_disposed(data);
|
|
1356
|
+
|
|
1130
1357
|
if (RB_TYPE_P(r_name, T_ARRAY))
|
|
1131
1358
|
{
|
|
1132
1359
|
long path_len = RARRAY_LEN(r_name);
|
|
@@ -1170,7 +1397,7 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1170
1397
|
{
|
|
1171
1398
|
VALUE r_first_str = rb_funcall(RARRAY_AREF(r_name, 0), rb_intern("to_s"), 0);
|
|
1172
1399
|
const char *first_seg = StringValueCStr(r_first_str);
|
|
1173
|
-
j_parent = JS_Eval(data->context, first_seg, strlen(first_seg),
|
|
1400
|
+
j_parent = JS_Eval(data->context, first_seg, strlen(first_seg), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
|
|
1174
1401
|
|
|
1175
1402
|
if (JS_IsException(j_parent) || !JS_IsObject(j_parent))
|
|
1176
1403
|
{
|
|
@@ -1247,6 +1474,7 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1247
1474
|
VMData *data;
|
|
1248
1475
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1249
1476
|
|
|
1477
|
+
check_disposed(data);
|
|
1250
1478
|
check_oom_poisoned(data);
|
|
1251
1479
|
|
|
1252
1480
|
JSValue j_this = JS_UNDEFINED;
|
|
@@ -1307,7 +1535,7 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1307
1535
|
const char *first_seg = StringValueCStr(r_first_str);
|
|
1308
1536
|
|
|
1309
1537
|
// JS_Eval accesses both global object properties and lexical (const/let) bindings
|
|
1310
|
-
JSValue j_cur = JS_Eval(data->context, first_seg, strlen(first_seg),
|
|
1538
|
+
JSValue j_cur = JS_Eval(data->context, first_seg, strlen(first_seg), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
|
|
1311
1539
|
if (JS_IsException(j_cur))
|
|
1312
1540
|
return to_rb_value(data->context, j_cur); // raises
|
|
1313
1541
|
|
|
@@ -1384,6 +1612,11 @@ static VALUE vm_m_set_module_loader(VALUE r_self, VALUE r_loader)
|
|
|
1384
1612
|
rb_raise(rb_eTypeError, "module_loader must be a Proc or nil");
|
|
1385
1613
|
|
|
1386
1614
|
data->module_loader = r_loader;
|
|
1615
|
+
// Stale entries from the previous loader's policy would survive the
|
|
1616
|
+
// swap and silently shadow the new behavior.
|
|
1617
|
+
rb_hash_clear(data->module_resolution_cache);
|
|
1618
|
+
rb_hash_clear(data->module_source_cache);
|
|
1619
|
+
register_module_loader_funcs(data);
|
|
1387
1620
|
return r_loader;
|
|
1388
1621
|
}
|
|
1389
1622
|
|
|
@@ -1394,6 +1627,17 @@ static VALUE vm_m_get_module_loader(VALUE r_self)
|
|
|
1394
1627
|
return data->module_loader;
|
|
1395
1628
|
}
|
|
1396
1629
|
|
|
1630
|
+
static VALUE vm_m_on_unhandled_rejection(VALUE r_self)
|
|
1631
|
+
{
|
|
1632
|
+
rb_need_block();
|
|
1633
|
+
|
|
1634
|
+
VMData *data;
|
|
1635
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1636
|
+
|
|
1637
|
+
data->on_unhandled_rejection = rb_block_proc();
|
|
1638
|
+
return Qnil;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1397
1641
|
static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
1398
1642
|
{
|
|
1399
1643
|
VALUE r_import_string, r_opts;
|
|
@@ -1408,12 +1652,17 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1408
1652
|
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
|
|
1409
1653
|
return Qnil;
|
|
1410
1654
|
}
|
|
1655
|
+
if (!NIL_P(r_from) && !NIL_P(r_filename))
|
|
1656
|
+
rb_raise(rb_eArgError, "pass either from: (inline source) or filename: (loader-resolved), not both");
|
|
1411
1657
|
VALUE r_custom_exposure = rb_hash_aref(r_opts, ID2SYM(rb_intern("code_to_expose")));
|
|
1412
1658
|
|
|
1413
1659
|
VMData *data;
|
|
1414
1660
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1415
1661
|
|
|
1662
|
+
check_disposed(data);
|
|
1663
|
+
|
|
1416
1664
|
char *filename;
|
|
1665
|
+
VALUE r_seeded_key = Qnil;
|
|
1417
1666
|
if (!NIL_P(r_filename))
|
|
1418
1667
|
{
|
|
1419
1668
|
filename = StringValueCStr(r_filename);
|
|
@@ -1429,6 +1678,18 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1429
1678
|
return to_rb_value(data->context, module);
|
|
1430
1679
|
}
|
|
1431
1680
|
js_module_set_import_meta(data->context, module, TRUE, FALSE);
|
|
1681
|
+
// The bridge module below will `import` this filename; without a
|
|
1682
|
+
// resolution-cache seed, our normalize hook would ask the user's
|
|
1683
|
+
// module_loader for it (and fail), even though QuickJS already has
|
|
1684
|
+
// the module loaded from the JS_Eval just above. Each `from:` call
|
|
1685
|
+
// mints a fresh random filename, so we also delete the entry once
|
|
1686
|
+
// the bridge eval finishes — otherwise the cache grows unboundedly.
|
|
1687
|
+
if (!NIL_P(data->module_loader))
|
|
1688
|
+
{
|
|
1689
|
+
VALUE r_filename_str = rb_str_new_cstr(filename);
|
|
1690
|
+
r_seeded_key = rb_ary_new3(2, r_filename_str, rb_str_new2(vmInternalFilename));
|
|
1691
|
+
rb_hash_aset(data->module_resolution_cache, r_seeded_key, r_filename_str);
|
|
1692
|
+
}
|
|
1432
1693
|
JS_FreeValue(data->context, module);
|
|
1433
1694
|
}
|
|
1434
1695
|
|
|
@@ -1456,9 +1717,21 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1456
1717
|
char *result = (char *)malloc(length + 1);
|
|
1457
1718
|
snprintf(result, length + 1, importAndGlobalizeModule, import_name, filename, globalize);
|
|
1458
1719
|
|
|
1459
|
-
JSValue j_codeResult = JS_Eval(data->context, result, strlen(result),
|
|
1720
|
+
JSValue j_codeResult = JS_Eval(data->context, result, strlen(result), vmInternalFilename, JS_EVAL_TYPE_MODULE);
|
|
1460
1721
|
free(result);
|
|
1461
|
-
|
|
1722
|
+
if (JS_IsException(j_codeResult))
|
|
1723
|
+
return to_rb_value(data->context, j_codeResult);
|
|
1724
|
+
|
|
1725
|
+
// Module eval returns a Promise. Awaiting it surfaces top-level throws,
|
|
1726
|
+
// rejected dynamic imports, and rejected top-level awaits as Ruby
|
|
1727
|
+
// exceptions instead of silently dropping them.
|
|
1728
|
+
JSValue j_awaited = js_std_await(data->context, j_codeResult);
|
|
1729
|
+
if (JS_IsException(j_awaited))
|
|
1730
|
+
return to_rb_value(data->context, j_awaited);
|
|
1731
|
+
JS_FreeValue(data->context, j_awaited);
|
|
1732
|
+
|
|
1733
|
+
if (!NIL_P(r_seeded_key))
|
|
1734
|
+
rb_hash_delete(data->module_resolution_cache, r_seeded_key);
|
|
1462
1735
|
|
|
1463
1736
|
return Qtrue;
|
|
1464
1737
|
}
|
|
@@ -1478,15 +1751,20 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
|
|
|
1478
1751
|
rb_define_method(r_class_vm, "eval_code", vm_m_evalCode, -1);
|
|
1479
1752
|
rb_define_private_method(r_class_vm, "_compile_to_bytecode", vm_m_compile, -1);
|
|
1480
1753
|
rb_define_private_method(r_class_vm, "_run_bytecode", vm_m_evalBytecode, 1);
|
|
1754
|
+
rb_define_private_method(r_class_vm, "_load_polyfill_bytecode", vm_m_loadPolyfillBytecode, 1);
|
|
1481
1755
|
rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
|
|
1482
1756
|
rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
|
|
1483
1757
|
rb_define_method(r_class_vm, "import", vm_m_import, -1);
|
|
1484
1758
|
rb_define_method(r_class_vm, "module_loader", vm_m_get_module_loader, 0);
|
|
1485
1759
|
rb_define_method(r_class_vm, "module_loader=", vm_m_set_module_loader, 1);
|
|
1760
|
+
rb_define_method(r_class_vm, "on_unhandled_rejection", vm_m_on_unhandled_rejection, 0);
|
|
1486
1761
|
rb_define_method(r_class_vm, "on_log", vm_m_on_log, 0);
|
|
1487
1762
|
rb_define_method(r_class_vm, "memory_usage", vm_m_memoryUsage, 0);
|
|
1488
1763
|
rb_define_method(r_class_vm, "gc!", vm_m_runGC, 0);
|
|
1489
1764
|
rb_define_method(r_class_vm, "memory_poisoned?", vm_m_memoryPoisoned, 0);
|
|
1765
|
+
rb_define_method(r_class_vm, "dispose!", vm_m_dispose, 0);
|
|
1766
|
+
rb_define_method(r_class_vm, "disposed?", vm_m_disposed, 0);
|
|
1767
|
+
rb_define_method(r_class_vm, "drain_jobs!", vm_m_drainJobs, 0);
|
|
1490
1768
|
r_define_log_class(r_class_vm);
|
|
1491
1769
|
}
|
|
1492
1770
|
|
|
@@ -1494,6 +1772,7 @@ static VALUE vm_m_memoryUsage(VALUE r_self)
|
|
|
1494
1772
|
{
|
|
1495
1773
|
VMData *data;
|
|
1496
1774
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1775
|
+
check_disposed(data);
|
|
1497
1776
|
JSMemoryUsage s;
|
|
1498
1777
|
JS_ComputeMemoryUsage(JS_GetRuntime(data->context), &s);
|
|
1499
1778
|
VALUE h = rb_hash_new();
|
|
@@ -1516,13 +1795,90 @@ static VALUE vm_m_runGC(VALUE r_self)
|
|
|
1516
1795
|
{
|
|
1517
1796
|
VMData *data;
|
|
1518
1797
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1798
|
+
check_disposed(data);
|
|
1519
1799
|
JS_RunGC(JS_GetRuntime(data->context));
|
|
1520
1800
|
return Qnil;
|
|
1521
1801
|
}
|
|
1522
1802
|
|
|
1803
|
+
static VALUE vm_m_drainJobs(VALUE r_self)
|
|
1804
|
+
{
|
|
1805
|
+
VMData *data;
|
|
1806
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1807
|
+
|
|
1808
|
+
check_oom_poisoned(data);
|
|
1809
|
+
|
|
1810
|
+
JSRuntime *runtime = JS_GetRuntime(data->context);
|
|
1811
|
+
if (!JS_IsJobPending(runtime))
|
|
1812
|
+
return INT2NUM(0);
|
|
1813
|
+
|
|
1814
|
+
arm_eval_timer(data);
|
|
1815
|
+
|
|
1816
|
+
int executed = 0;
|
|
1817
|
+
for (;;)
|
|
1818
|
+
{
|
|
1819
|
+
int err = JS_ExecutePendingJob(runtime, NULL);
|
|
1820
|
+
if (err == 0)
|
|
1821
|
+
break;
|
|
1822
|
+
if (err < 0)
|
|
1823
|
+
return to_rb_value(data->context, JS_EXCEPTION); // raises
|
|
1824
|
+
executed++;
|
|
1825
|
+
}
|
|
1826
|
+
return INT2NUM(executed);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1523
1829
|
static VALUE vm_m_memoryPoisoned(VALUE r_self)
|
|
1524
1830
|
{
|
|
1525
1831
|
VMData *data;
|
|
1526
1832
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1527
1833
|
return data->oom_poisoned ? Qtrue : Qfalse;
|
|
1528
1834
|
}
|
|
1835
|
+
|
|
1836
|
+
// JS_FreeContext + JS_FreeRuntime walk the entire heap to run finalisers.
|
|
1837
|
+
// On a VM with polyfills loaded this can be tens of milliseconds — run it
|
|
1838
|
+
// without the GVL so other Ruby threads (e.g. the next pool builder) keep
|
|
1839
|
+
// progressing. Safe to release because nothing in the teardown path calls
|
|
1840
|
+
// back into Ruby: module_loader, console, and define_function callbacks
|
|
1841
|
+
// only fire during JS execution, not during free.
|
|
1842
|
+
static void *vm_dispose_no_gvl(void *p)
|
|
1843
|
+
{
|
|
1844
|
+
vm_teardown_context((JSContext *)p);
|
|
1845
|
+
return NULL;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
static VALUE vm_m_dispose(VALUE r_self)
|
|
1849
|
+
{
|
|
1850
|
+
VMData *data;
|
|
1851
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1852
|
+
|
|
1853
|
+
if (data->disposed)
|
|
1854
|
+
return Qnil;
|
|
1855
|
+
|
|
1856
|
+
if (!JS_IsUndefined(data->j_file_proxy_creator))
|
|
1857
|
+
{
|
|
1858
|
+
JS_FreeValue(data->context, data->j_file_proxy_creator);
|
|
1859
|
+
data->j_file_proxy_creator = JS_UNDEFINED;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Mark disposed before releasing the GVL so a concurrent dfree finds
|
|
1863
|
+
// disposed=true and skips its own teardown.
|
|
1864
|
+
data->disposed = true;
|
|
1865
|
+
|
|
1866
|
+
rb_thread_call_without_gvl(vm_dispose_no_gvl, data->context, NULL, NULL);
|
|
1867
|
+
|
|
1868
|
+
// Drop references to user-supplied closures so Ruby GC can reclaim them
|
|
1869
|
+
// (and anything they captured) before the wrapping VM object itself is
|
|
1870
|
+
// collected. Matters for pool-rebuild workloads that dispose eagerly.
|
|
1871
|
+
data->defined_functions = rb_hash_new();
|
|
1872
|
+
data->alive_objects = rb_hash_new();
|
|
1873
|
+
data->log_listener = Qnil;
|
|
1874
|
+
data->module_loader = Qnil;
|
|
1875
|
+
|
|
1876
|
+
return Qnil;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
static VALUE vm_m_disposed(VALUE r_self)
|
|
1880
|
+
{
|
|
1881
|
+
VMData *data;
|
|
1882
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1883
|
+
return data->disposed ? Qtrue : Qfalse;
|
|
1884
|
+
}
|