quickjs 0.17.0.rc1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +102 -3
- data/ext/quickjsrb/quickjsrb.c +357 -25
- data/ext/quickjsrb/quickjsrb.h +39 -7
- data/ext/quickjsrb/vendor/polyfill-intl-en.min.js +4 -4
- data/lib/quickjs/version.rb +1 -1
- data/lib/quickjs.rb +19 -5
- data/polyfills/package-lock.json +110 -110
- data/polyfills/package.json +1 -1
- data/sig/quickjs.rbs +14 -2
- metadata +1 -1
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)
|
|
599
|
+
{
|
|
600
|
+
JS_ThrowReferenceError(ctx, "module loader returned no source for '%s'", name);
|
|
601
|
+
return NULL;
|
|
602
|
+
}
|
|
603
|
+
|
|
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
|
|
545
626
|
{
|
|
546
|
-
|
|
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));
|
|
547
629
|
return NULL;
|
|
548
630
|
}
|
|
549
631
|
|
|
550
|
-
|
|
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))))
|
|
@@ -892,7 +1080,7 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
892
1080
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillIntlId))))
|
|
893
1081
|
{
|
|
894
1082
|
const char *defineIntl = "Object.defineProperty(globalThis, 'Intl', { value:{} });\n";
|
|
895
|
-
JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl),
|
|
1083
|
+
JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
|
|
896
1084
|
JS_FreeValue(data->context, j_defineIntl);
|
|
897
1085
|
|
|
898
1086
|
JSValue j_polyfillIntlResult = load_polyfill_bytecode(data->context, &qjsc_polyfill_intl_en_min, qjsc_polyfill_intl_en_min_size);
|
|
@@ -984,6 +1172,15 @@ static void check_oom_poisoned(VMData *data)
|
|
|
984
1172
|
}
|
|
985
1173
|
}
|
|
986
1174
|
|
|
1175
|
+
static void check_disposed(VMData *data)
|
|
1176
|
+
{
|
|
1177
|
+
if (data->disposed)
|
|
1178
|
+
{
|
|
1179
|
+
VALUE r_msg = rb_str_new2("VM has been disposed; create a new Quickjs::VM");
|
|
1180
|
+
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_msg, Qnil));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
987
1184
|
// Validate that r_code is a String and resolve the :filename option (default "<code>")
|
|
988
1185
|
// from the keyword-args hash that rb_scan_args(... "1:") collects. Both eval_code and
|
|
989
1186
|
// compile share this argument shape.
|
|
@@ -1014,6 +1211,7 @@ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
|
|
|
1014
1211
|
VMData *data;
|
|
1015
1212
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1016
1213
|
|
|
1214
|
+
check_disposed(data);
|
|
1017
1215
|
check_oom_poisoned(data);
|
|
1018
1216
|
|
|
1019
1217
|
VALUE r_code, r_opts;
|
|
@@ -1052,6 +1250,8 @@ static VALUE vm_m_compile(int argc, VALUE *argv, VALUE r_self)
|
|
|
1052
1250
|
VMData *data;
|
|
1053
1251
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1054
1252
|
|
|
1253
|
+
check_disposed(data);
|
|
1254
|
+
|
|
1055
1255
|
VALUE r_code, r_opts;
|
|
1056
1256
|
rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
|
|
1057
1257
|
const char *filename = parse_code_and_filename(r_code, r_opts);
|
|
@@ -1086,6 +1286,8 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1086
1286
|
VMData *data;
|
|
1087
1287
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1088
1288
|
|
|
1289
|
+
check_disposed(data);
|
|
1290
|
+
|
|
1089
1291
|
if (!RB_TYPE_P(r_bytecode, T_STRING))
|
|
1090
1292
|
{
|
|
1091
1293
|
VALUE r_class = rb_class_name(CLASS_OF(r_bytecode));
|
|
@@ -1127,6 +1329,8 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1127
1329
|
VMData *data;
|
|
1128
1330
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1129
1331
|
|
|
1332
|
+
check_disposed(data);
|
|
1333
|
+
|
|
1130
1334
|
if (RB_TYPE_P(r_name, T_ARRAY))
|
|
1131
1335
|
{
|
|
1132
1336
|
long path_len = RARRAY_LEN(r_name);
|
|
@@ -1170,7 +1374,7 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1170
1374
|
{
|
|
1171
1375
|
VALUE r_first_str = rb_funcall(RARRAY_AREF(r_name, 0), rb_intern("to_s"), 0);
|
|
1172
1376
|
const char *first_seg = StringValueCStr(r_first_str);
|
|
1173
|
-
j_parent = JS_Eval(data->context, first_seg, strlen(first_seg),
|
|
1377
|
+
j_parent = JS_Eval(data->context, first_seg, strlen(first_seg), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
|
|
1174
1378
|
|
|
1175
1379
|
if (JS_IsException(j_parent) || !JS_IsObject(j_parent))
|
|
1176
1380
|
{
|
|
@@ -1247,6 +1451,7 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1247
1451
|
VMData *data;
|
|
1248
1452
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1249
1453
|
|
|
1454
|
+
check_disposed(data);
|
|
1250
1455
|
check_oom_poisoned(data);
|
|
1251
1456
|
|
|
1252
1457
|
JSValue j_this = JS_UNDEFINED;
|
|
@@ -1307,7 +1512,7 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1307
1512
|
const char *first_seg = StringValueCStr(r_first_str);
|
|
1308
1513
|
|
|
1309
1514
|
// 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),
|
|
1515
|
+
JSValue j_cur = JS_Eval(data->context, first_seg, strlen(first_seg), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
|
|
1311
1516
|
if (JS_IsException(j_cur))
|
|
1312
1517
|
return to_rb_value(data->context, j_cur); // raises
|
|
1313
1518
|
|
|
@@ -1384,6 +1589,11 @@ static VALUE vm_m_set_module_loader(VALUE r_self, VALUE r_loader)
|
|
|
1384
1589
|
rb_raise(rb_eTypeError, "module_loader must be a Proc or nil");
|
|
1385
1590
|
|
|
1386
1591
|
data->module_loader = r_loader;
|
|
1592
|
+
// Stale entries from the previous loader's policy would survive the
|
|
1593
|
+
// swap and silently shadow the new behavior.
|
|
1594
|
+
rb_hash_clear(data->module_resolution_cache);
|
|
1595
|
+
rb_hash_clear(data->module_source_cache);
|
|
1596
|
+
register_module_loader_funcs(data);
|
|
1387
1597
|
return r_loader;
|
|
1388
1598
|
}
|
|
1389
1599
|
|
|
@@ -1394,6 +1604,17 @@ static VALUE vm_m_get_module_loader(VALUE r_self)
|
|
|
1394
1604
|
return data->module_loader;
|
|
1395
1605
|
}
|
|
1396
1606
|
|
|
1607
|
+
static VALUE vm_m_on_unhandled_rejection(VALUE r_self)
|
|
1608
|
+
{
|
|
1609
|
+
rb_need_block();
|
|
1610
|
+
|
|
1611
|
+
VMData *data;
|
|
1612
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1613
|
+
|
|
1614
|
+
data->on_unhandled_rejection = rb_block_proc();
|
|
1615
|
+
return Qnil;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1397
1618
|
static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
1398
1619
|
{
|
|
1399
1620
|
VALUE r_import_string, r_opts;
|
|
@@ -1408,12 +1629,17 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1408
1629
|
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
|
|
1409
1630
|
return Qnil;
|
|
1410
1631
|
}
|
|
1632
|
+
if (!NIL_P(r_from) && !NIL_P(r_filename))
|
|
1633
|
+
rb_raise(rb_eArgError, "pass either from: (inline source) or filename: (loader-resolved), not both");
|
|
1411
1634
|
VALUE r_custom_exposure = rb_hash_aref(r_opts, ID2SYM(rb_intern("code_to_expose")));
|
|
1412
1635
|
|
|
1413
1636
|
VMData *data;
|
|
1414
1637
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1415
1638
|
|
|
1639
|
+
check_disposed(data);
|
|
1640
|
+
|
|
1416
1641
|
char *filename;
|
|
1642
|
+
VALUE r_seeded_key = Qnil;
|
|
1417
1643
|
if (!NIL_P(r_filename))
|
|
1418
1644
|
{
|
|
1419
1645
|
filename = StringValueCStr(r_filename);
|
|
@@ -1429,6 +1655,18 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1429
1655
|
return to_rb_value(data->context, module);
|
|
1430
1656
|
}
|
|
1431
1657
|
js_module_set_import_meta(data->context, module, TRUE, FALSE);
|
|
1658
|
+
// The bridge module below will `import` this filename; without a
|
|
1659
|
+
// resolution-cache seed, our normalize hook would ask the user's
|
|
1660
|
+
// module_loader for it (and fail), even though QuickJS already has
|
|
1661
|
+
// the module loaded from the JS_Eval just above. Each `from:` call
|
|
1662
|
+
// mints a fresh random filename, so we also delete the entry once
|
|
1663
|
+
// the bridge eval finishes — otherwise the cache grows unboundedly.
|
|
1664
|
+
if (!NIL_P(data->module_loader))
|
|
1665
|
+
{
|
|
1666
|
+
VALUE r_filename_str = rb_str_new_cstr(filename);
|
|
1667
|
+
r_seeded_key = rb_ary_new3(2, r_filename_str, rb_str_new2(vmInternalFilename));
|
|
1668
|
+
rb_hash_aset(data->module_resolution_cache, r_seeded_key, r_filename_str);
|
|
1669
|
+
}
|
|
1432
1670
|
JS_FreeValue(data->context, module);
|
|
1433
1671
|
}
|
|
1434
1672
|
|
|
@@ -1456,9 +1694,21 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1456
1694
|
char *result = (char *)malloc(length + 1);
|
|
1457
1695
|
snprintf(result, length + 1, importAndGlobalizeModule, import_name, filename, globalize);
|
|
1458
1696
|
|
|
1459
|
-
JSValue j_codeResult = JS_Eval(data->context, result, strlen(result),
|
|
1697
|
+
JSValue j_codeResult = JS_Eval(data->context, result, strlen(result), vmInternalFilename, JS_EVAL_TYPE_MODULE);
|
|
1460
1698
|
free(result);
|
|
1461
|
-
|
|
1699
|
+
if (JS_IsException(j_codeResult))
|
|
1700
|
+
return to_rb_value(data->context, j_codeResult);
|
|
1701
|
+
|
|
1702
|
+
// Module eval returns a Promise. Awaiting it surfaces top-level throws,
|
|
1703
|
+
// rejected dynamic imports, and rejected top-level awaits as Ruby
|
|
1704
|
+
// exceptions instead of silently dropping them.
|
|
1705
|
+
JSValue j_awaited = js_std_await(data->context, j_codeResult);
|
|
1706
|
+
if (JS_IsException(j_awaited))
|
|
1707
|
+
return to_rb_value(data->context, j_awaited);
|
|
1708
|
+
JS_FreeValue(data->context, j_awaited);
|
|
1709
|
+
|
|
1710
|
+
if (!NIL_P(r_seeded_key))
|
|
1711
|
+
rb_hash_delete(data->module_resolution_cache, r_seeded_key);
|
|
1462
1712
|
|
|
1463
1713
|
return Qtrue;
|
|
1464
1714
|
}
|
|
@@ -1483,10 +1733,14 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
|
|
|
1483
1733
|
rb_define_method(r_class_vm, "import", vm_m_import, -1);
|
|
1484
1734
|
rb_define_method(r_class_vm, "module_loader", vm_m_get_module_loader, 0);
|
|
1485
1735
|
rb_define_method(r_class_vm, "module_loader=", vm_m_set_module_loader, 1);
|
|
1736
|
+
rb_define_method(r_class_vm, "on_unhandled_rejection", vm_m_on_unhandled_rejection, 0);
|
|
1486
1737
|
rb_define_method(r_class_vm, "on_log", vm_m_on_log, 0);
|
|
1487
1738
|
rb_define_method(r_class_vm, "memory_usage", vm_m_memoryUsage, 0);
|
|
1488
1739
|
rb_define_method(r_class_vm, "gc!", vm_m_runGC, 0);
|
|
1489
1740
|
rb_define_method(r_class_vm, "memory_poisoned?", vm_m_memoryPoisoned, 0);
|
|
1741
|
+
rb_define_method(r_class_vm, "dispose!", vm_m_dispose, 0);
|
|
1742
|
+
rb_define_method(r_class_vm, "disposed?", vm_m_disposed, 0);
|
|
1743
|
+
rb_define_method(r_class_vm, "drain_jobs!", vm_m_drainJobs, 0);
|
|
1490
1744
|
r_define_log_class(r_class_vm);
|
|
1491
1745
|
}
|
|
1492
1746
|
|
|
@@ -1494,6 +1748,7 @@ static VALUE vm_m_memoryUsage(VALUE r_self)
|
|
|
1494
1748
|
{
|
|
1495
1749
|
VMData *data;
|
|
1496
1750
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1751
|
+
check_disposed(data);
|
|
1497
1752
|
JSMemoryUsage s;
|
|
1498
1753
|
JS_ComputeMemoryUsage(JS_GetRuntime(data->context), &s);
|
|
1499
1754
|
VALUE h = rb_hash_new();
|
|
@@ -1516,13 +1771,90 @@ static VALUE vm_m_runGC(VALUE r_self)
|
|
|
1516
1771
|
{
|
|
1517
1772
|
VMData *data;
|
|
1518
1773
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1774
|
+
check_disposed(data);
|
|
1519
1775
|
JS_RunGC(JS_GetRuntime(data->context));
|
|
1520
1776
|
return Qnil;
|
|
1521
1777
|
}
|
|
1522
1778
|
|
|
1779
|
+
static VALUE vm_m_drainJobs(VALUE r_self)
|
|
1780
|
+
{
|
|
1781
|
+
VMData *data;
|
|
1782
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1783
|
+
|
|
1784
|
+
check_oom_poisoned(data);
|
|
1785
|
+
|
|
1786
|
+
JSRuntime *runtime = JS_GetRuntime(data->context);
|
|
1787
|
+
if (!JS_IsJobPending(runtime))
|
|
1788
|
+
return INT2NUM(0);
|
|
1789
|
+
|
|
1790
|
+
arm_eval_timer(data);
|
|
1791
|
+
|
|
1792
|
+
int executed = 0;
|
|
1793
|
+
for (;;)
|
|
1794
|
+
{
|
|
1795
|
+
int err = JS_ExecutePendingJob(runtime, NULL);
|
|
1796
|
+
if (err == 0)
|
|
1797
|
+
break;
|
|
1798
|
+
if (err < 0)
|
|
1799
|
+
return to_rb_value(data->context, JS_EXCEPTION); // raises
|
|
1800
|
+
executed++;
|
|
1801
|
+
}
|
|
1802
|
+
return INT2NUM(executed);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1523
1805
|
static VALUE vm_m_memoryPoisoned(VALUE r_self)
|
|
1524
1806
|
{
|
|
1525
1807
|
VMData *data;
|
|
1526
1808
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1527
1809
|
return data->oom_poisoned ? Qtrue : Qfalse;
|
|
1528
1810
|
}
|
|
1811
|
+
|
|
1812
|
+
// JS_FreeContext + JS_FreeRuntime walk the entire heap to run finalisers.
|
|
1813
|
+
// On a VM with polyfills loaded this can be tens of milliseconds — run it
|
|
1814
|
+
// without the GVL so other Ruby threads (e.g. the next pool builder) keep
|
|
1815
|
+
// progressing. Safe to release because nothing in the teardown path calls
|
|
1816
|
+
// back into Ruby: module_loader, console, and define_function callbacks
|
|
1817
|
+
// only fire during JS execution, not during free.
|
|
1818
|
+
static void *vm_dispose_no_gvl(void *p)
|
|
1819
|
+
{
|
|
1820
|
+
vm_teardown_context((JSContext *)p);
|
|
1821
|
+
return NULL;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
static VALUE vm_m_dispose(VALUE r_self)
|
|
1825
|
+
{
|
|
1826
|
+
VMData *data;
|
|
1827
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1828
|
+
|
|
1829
|
+
if (data->disposed)
|
|
1830
|
+
return Qnil;
|
|
1831
|
+
|
|
1832
|
+
if (!JS_IsUndefined(data->j_file_proxy_creator))
|
|
1833
|
+
{
|
|
1834
|
+
JS_FreeValue(data->context, data->j_file_proxy_creator);
|
|
1835
|
+
data->j_file_proxy_creator = JS_UNDEFINED;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// Mark disposed before releasing the GVL so a concurrent dfree finds
|
|
1839
|
+
// disposed=true and skips its own teardown.
|
|
1840
|
+
data->disposed = true;
|
|
1841
|
+
|
|
1842
|
+
rb_thread_call_without_gvl(vm_dispose_no_gvl, data->context, NULL, NULL);
|
|
1843
|
+
|
|
1844
|
+
// Drop references to user-supplied closures so Ruby GC can reclaim them
|
|
1845
|
+
// (and anything they captured) before the wrapping VM object itself is
|
|
1846
|
+
// collected. Matters for pool-rebuild workloads that dispose eagerly.
|
|
1847
|
+
data->defined_functions = rb_hash_new();
|
|
1848
|
+
data->alive_objects = rb_hash_new();
|
|
1849
|
+
data->log_listener = Qnil;
|
|
1850
|
+
data->module_loader = Qnil;
|
|
1851
|
+
|
|
1852
|
+
return Qnil;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
static VALUE vm_m_disposed(VALUE r_self)
|
|
1856
|
+
{
|
|
1857
|
+
VMData *data;
|
|
1858
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1859
|
+
return data->disposed ? Qtrue : Qfalse;
|
|
1860
|
+
}
|