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.
@@ -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, to_rb_value(ctx, j_elem));
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), to_rb_value(ctx, j_prop));
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
- // Fallback: non-plain objects (Date, RegExp, Map, class instances, functions, etc.)
350
- VALUE r_str = to_r_json(ctx, j_val);
351
-
352
- if (rb_funcall(r_str, rb_intern("=="), 1, rb_str_new2("undefined")))
353
- {
354
- return QUICKJSRB_SYM(undefinedId);
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, js_module_loader, js_module_check_attributes, 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 j_polyfillIntlObject = JS_ReadObject(data->context, &qjsc_polyfill_intl_en_min, qjsc_polyfill_intl_en_min_size, JS_READ_OBJ_BYTECODE);
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 j_polyfillFileObject = JS_ReadObject(data->context, &qjsc_polyfill_file_min, qjsc_polyfill_file_min_size, JS_READ_OBJ_BYTECODE);
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 j_polyfillEncodingObject = JS_ReadObject(data->context, &qjsc_polyfill_encoding_min, qjsc_polyfill_encoding_min_size, JS_READ_OBJ_BYTECODE);
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 j_polyfillUrlObject = JS_ReadObject(data->context, &qjsc_polyfill_url_min, qjsc_polyfill_url_min_size, JS_READ_OBJ_BYTECODE);
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 VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
978
+ static void check_oom_poisoned(VMData *data)
851
979
  {
852
- VMData *data;
853
- TypedData_Get_Struct(r_self, VMData, &vm_type, data);
854
-
855
- VALUE r_code, r_opts;
856
- rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
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
- const char *filename = "<code>";
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 r_filename = rb_hash_aref(r_opts, ID2SYM(rb_intern("filename")));
868
- if (!NIL_P(r_filename))
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
- clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
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
- if (NIL_P(r_from))
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 = random_string();
1165
- char *source = StringValueCStr(r_from);
1166
- JSValue module = JS_Eval(data->context, source, strlen(source), filename, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
1167
- if (JS_IsException(module))
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
+ }
@@ -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 *runtime = JS_NewRuntime();
122
- data->context = JS_NewContext(runtime);
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
  }