quickjs 0.16.1 → 0.17.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed97f8a3a6eb6b10b53cf9e3db69e0074cb962be7908c033c003c39a104ec239
4
- data.tar.gz: cb75d33d9d098d79bfaba4f3ecb7f47ca03d2ee91c95c2a73f8fa2e5b49d1337
3
+ metadata.gz: 7b233c0e106654a50f4af274fef6a4dd77f0b53cc0a703473cda844da6ba3763
4
+ data.tar.gz: 50dfeaf56422ee74e8cb8711a58e1678c8141ac4bb7256a4d8861fa1a9415c93
5
5
  SHA512:
6
- metadata.gz: 3fb3817ae3bba2dae090a0a4906e3477c543fba0795d7763b33f66fa4409270a2148149d27a589907b8e4eae1eebc67d9be93d62feaa8f6f8475542e126c0489
7
- data.tar.gz: 4645cf66429e2ef0ce2ec6b7f86f8a4ef8a210255d0ea7e3d7613b9974a4bca3b6a1374113fd19af4da69e8262c5f8ac5f6436e87f71c248fda23a863f73a376
6
+ metadata.gz: 9f77f8b802055b1ec67ad9b4f45fb54cbdb1349a473457572c1c1b72115283bed9b99b15029e101256147ce0bbc72b64371fcbdd0ede6e8e9ba5e8d4a0bb0376
7
+ data.tar.gz: 9387e82c41c0242caa67c16a67b48a204c32512b116c5e4cc61d83e892bec053393facaf7ac2553642b876cacecb3622ddb37a846fd1eea959c21ea7df70b569
data/README.md CHANGED
@@ -85,6 +85,21 @@ vm.eval_code('a.b = "d";')
85
85
  vm.eval_code('a.b;') #=> "d"
86
86
  ```
87
87
 
88
+ #### `Quickjs::VM#compile`: 🚀 Cache parsed bundles as a `Quickjs::Runnable`
89
+
90
+ Parsing large JS bundles is the dominant cost of a fresh evaluation. `compile` parses once and returns a `Quickjs::Runnable` wrapping the serialized bytecode; `run(on:)` executes it on any VM of the same QuickJS build, skipping the parser. Useful when the same bundle is evaluated repeatedly across short-lived VMs (test environments, page-per-VM web emulators).
91
+
92
+ ```rb
93
+ runnable = Quickjs::VM.new.compile(File.read('big_bundle.js'), filename: 'big_bundle.js')
94
+
95
+ vm = Quickjs::VM.new
96
+ runnable.run(on: vm) # use the given VM (no parse cost)
97
+ runnable.run # spin up a fresh VM with default options
98
+ runnable.run(on: { features: [::Quickjs::POLYFILL_INTL] }) # ad-hoc VM with options
99
+ ```
100
+
101
+ `Runnable#to_s` returns the underlying bytecode as a frozen ASCII-8BIT `String`, suitable for caching to memory or disk. `Quickjs::Runnable.new(bytecode_string)` reconstructs a `Runnable` from that blob — validation happens lazily at `run` time, so a corrupt or wrong-build blob surfaces as `Quickjs::RuntimeError` when executed. The bytecode format is tied to the QuickJS build, so include the gem version in your cache key if you persist across upgrades.
102
+
88
103
  #### `Quickjs::VM#call`: ⚡ Call a JS function directly with Ruby arguments
89
104
 
90
105
  ```rb
@@ -213,6 +228,37 @@ vm.eval_code('console.log("hello", 42)')
213
228
  # log.raw #=> Array of raw Ruby values
214
229
  ```
215
230
 
231
+ #### Memory management: 🔍 Inspect and control VM memory
232
+
233
+ ```rb
234
+ vm = Quickjs::VM.new
235
+
236
+ vm.memory_usage
237
+ # => { malloc_size: Integer, malloc_limit: Integer, memory_used_size: Integer,
238
+ # atom_count: Integer, str_count: Integer, obj_count: Integer,
239
+ # prop_count: Integer, shape_count: Integer,
240
+ # js_func_count: Integer, js_func_code_size: Integer,
241
+ # c_func_count: Integer, array_count: Integer }
242
+
243
+ vm.gc! # trigger a QuickJS GC cycle; returns nil
244
+
245
+ vm.memory_poisoned? #=> false (true once the VM has hit out-of-memory)
246
+ ```
247
+
248
+ When the JS heap exhausts its memory limit, QuickJS enters a fragile state where further evaluation can segfault the process. `memory_poisoned?` flips to `true` after such an event, and subsequent `eval_code` / `call` calls raise `Quickjs::RuntimeError` immediately instead of risking a crash. Rescue it and recreate the VM.
249
+
250
+ ```rb
251
+ vm = Quickjs::VM.new(memory_limit: 256 * 1024 * 1024)
252
+
253
+ begin
254
+ vm.eval_code(js)
255
+ rescue Quickjs::RuntimeError => e
256
+ raise unless vm.memory_poisoned?
257
+ vm = Quickjs::VM.new(memory_limit: 256 * 1024 * 1024)
258
+ retry
259
+ end
260
+ ```
261
+
216
262
  ### Value Conversion
217
263
 
218
264
  | JavaScript | | Ruby | Note |
@@ -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
  {
@@ -344,30 +365,38 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
344
365
  return r_maybe_file;
345
366
  }
346
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
+
347
378
  if (JS_IsArray(ctx, j_val))
348
- return js_array_to_rb(ctx, j_val);
379
+ return js_array_to_rb(ctx, j_val, r_visited);
349
380
 
350
381
  if (js_is_plain_object(ctx, j_val))
351
- return js_plain_object_to_rb(ctx, j_val);
352
-
353
- // Fallback: non-plain objects (Date, RegExp, Map, class instances, functions, etc.)
354
- VALUE r_str = to_r_json(ctx, j_val);
355
-
356
- if (rb_funcall(r_str, rb_intern("=="), 1, rb_str_new2("undefined")))
357
- {
358
- return QUICKJSRB_SYM(undefinedId);
359
- }
360
-
361
- int couldntParse;
362
- VALUE r_result = rb_protect(r_try_json_parse, r_str, &couldntParse);
363
- if (couldntParse)
364
- {
365
- return Qnil;
366
- }
367
- 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))
368
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);
369
396
  return r_result;
370
397
  }
398
+ JS_FreeValue(ctx, j_toJSON);
399
+ return js_plain_object_to_rb(ctx, j_val, r_visited);
371
400
  }
372
401
  case JS_TAG_NULL:
373
402
  return Qnil;
@@ -423,6 +452,14 @@ VALUE to_rb_value(JSContext *ctx, JSValue j_val)
423
452
  {
424
453
  r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_INTERRUPTED_ERROR);
425
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
+ }
426
463
  else
427
464
  {
428
465
  r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
@@ -759,6 +796,40 @@ static JSValue js_console_error(JSContext *ctx, JSValueConst this, int argc, JSV
759
796
  return js_quickjsrb_log(ctx, this, argc, argv, "error");
760
797
  }
761
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
+
762
833
  static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
763
834
  {
764
835
  VALUE r_opts;
@@ -824,15 +895,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
824
895
  JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), "<vm>", JS_EVAL_TYPE_GLOBAL);
825
896
  JS_FreeValue(data->context, j_defineIntl);
826
897
 
827
- JSValue j_polyfillIntlObject = JS_ReadObject(data->context, &qjsc_polyfill_intl_en_min, qjsc_polyfill_intl_en_min_size, JS_READ_OBJ_BYTECODE);
828
- 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);
829
899
  JS_FreeValue(data->context, j_polyfillIntlResult);
830
900
  }
831
901
 
832
902
  if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillFileId))))
833
903
  {
834
- JSValue j_polyfillFileObject = JS_ReadObject(data->context, &qjsc_polyfill_file_min, qjsc_polyfill_file_min_size, JS_READ_OBJ_BYTECODE);
835
- 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);
836
905
  JS_FreeValue(data->context, j_polyfillFileResult);
837
906
 
838
907
  quickjsrb_init_file_proxy(data);
@@ -840,15 +909,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
840
909
 
841
910
  if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillEncodingId))))
842
911
  {
843
- JSValue j_polyfillEncodingObject = JS_ReadObject(data->context, &qjsc_polyfill_encoding_min, qjsc_polyfill_encoding_min_size, JS_READ_OBJ_BYTECODE);
844
- 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);
845
913
  JS_FreeValue(data->context, j_polyfillEncodingResult);
846
914
  }
847
915
 
848
916
  if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillUrlId))))
849
917
  {
850
- JSValue j_polyfillUrlObject = JS_ReadObject(data->context, &qjsc_polyfill_url_min, qjsc_polyfill_url_min_size, JS_READ_OBJ_BYTECODE);
851
- 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);
852
919
  JS_FreeValue(data->context, j_polyfillUrlResult);
853
920
  }
854
921
 
@@ -857,6 +924,10 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
857
924
  quickjsrb_init_crypto(data->context, j_global);
858
925
  }
859
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.
860
931
  JSValue j_console = JS_NewObject(data->context);
861
932
  JS_SetPropertyStr(
862
933
  data->context, j_console, "log",
@@ -904,35 +975,69 @@ static VALUE to_rb_return_value(JSContext *ctx, JSValue j_val)
904
975
  return result;
905
976
  }
906
977
 
907
- static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
978
+ static void check_oom_poisoned(VMData *data)
908
979
  {
909
- VMData *data;
910
- TypedData_Get_Struct(r_self, VMData, &vm_type, data);
911
-
912
- VALUE r_code, r_opts;
913
- 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
+ }
914
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
+ {
915
992
  if (!RB_TYPE_P(r_code, T_STRING))
916
993
  {
917
994
  VALUE r_code_class = rb_class_name(CLASS_OF(r_code));
918
995
  rb_raise(rb_eTypeError, "JavaScript code must be a String, got %s", StringValueCStr(r_code_class));
919
996
  }
997
+ if (NIL_P(r_opts))
998
+ return "<code>";
999
+ VALUE r_filename = rb_hash_aref(r_opts, ID2SYM(rb_intern("filename")));
1000
+ if (NIL_P(r_filename))
1001
+ return "<code>";
1002
+ Check_Type(r_filename, T_STRING);
1003
+ return StringValueCStr(r_filename);
1004
+ }
1005
+
1006
+ static void arm_eval_timer(VMData *data)
1007
+ {
1008
+ clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
1009
+ JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
1010
+ }
1011
+
1012
+ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
1013
+ {
1014
+ VMData *data;
1015
+ TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1016
+
1017
+ check_oom_poisoned(data);
1018
+
1019
+ VALUE r_code, r_opts;
1020
+ rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
1021
+ const char *filename = parse_code_and_filename(r_code, r_opts);
920
1022
 
921
- const char *filename = "<code>";
1023
+ bool async_mode = true;
922
1024
  if (!NIL_P(r_opts))
923
1025
  {
924
- VALUE r_filename = rb_hash_aref(r_opts, ID2SYM(rb_intern("filename")));
925
- if (!NIL_P(r_filename))
926
- {
927
- Check_Type(r_filename, T_STRING);
928
- filename = StringValueCStr(r_filename);
929
- }
1026
+ VALUE r_async = rb_hash_aref(r_opts, ID2SYM(rb_intern("async")));
1027
+ if (r_async == Qfalse)
1028
+ async_mode = false;
930
1029
  }
931
1030
 
932
- clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
933
- JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
1031
+ arm_eval_timer(data);
934
1032
 
935
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
+
936
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);
937
1042
  JSValue j_awaitedResult = js_std_await(data->context, j_codeResult); // This frees j_codeResult
938
1043
  // JS_EVAL_FLAG_ASYNC wraps the result in {value, done} — extract the actual value
@@ -942,6 +1047,74 @@ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
942
1047
  return to_rb_return_value(data->context, j_returnedValue);
943
1048
  }
944
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
+
945
1118
  static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
946
1119
  {
947
1120
  rb_need_block();
@@ -1074,6 +1247,8 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
1074
1247
  VMData *data;
1075
1248
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1076
1249
 
1250
+ check_oom_poisoned(data);
1251
+
1077
1252
  JSValue j_this = JS_UNDEFINED;
1078
1253
  JSValue j_func;
1079
1254
 
@@ -1301,11 +1476,53 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
1301
1476
  rb_define_alloc_func(r_class_vm, vm_alloc);
1302
1477
  rb_define_method(r_class_vm, "initialize", vm_m_initialize, -1);
1303
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);
1304
1481
  rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
1305
1482
  rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
1306
1483
  rb_define_method(r_class_vm, "import", vm_m_import, -1);
1307
1484
  rb_define_method(r_class_vm, "module_loader", vm_m_get_module_loader, 0);
1308
1485
  rb_define_method(r_class_vm, "module_loader=", vm_m_set_module_loader, 1);
1309
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);
1310
1490
  r_define_log_class(r_class_vm);
1311
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"
@@ -56,6 +58,12 @@ typedef struct VMData
56
58
  VALUE alive_objects;
57
59
  VALUE module_loader;
58
60
  JSValue j_file_proxy_creator;
61
+ // Once the runtime has hit JS-level "out of memory", the QuickJS heap is in
62
+ // a fragile state where further evaluation can trigger a use-after-free in
63
+ // the parser-error-during-OOM cascade (segfault inside js_shape_hash_unlink).
64
+ // Trip this flag so subsequent eval_code/call calls refuse cleanly with a
65
+ // Ruby exception instead of risking a process crash.
66
+ bool oom_poisoned;
59
67
  } VMData;
60
68
 
61
69
  static void vm_free(void *ptr)
@@ -109,6 +117,18 @@ static const rb_data_type_t vm_type = {
109
117
  .flags = RUBY_TYPED_FREE_IMMEDIATELY,
110
118
  };
111
119
 
120
+ struct vm_create_args
121
+ {
122
+ JSContext *context;
123
+ };
124
+
125
+ static void *vm_create_no_gvl(void *p)
126
+ {
127
+ struct vm_create_args *args = p;
128
+ args->context = JS_NewContext(JS_NewRuntime());
129
+ return NULL;
130
+ }
131
+
112
132
  static VALUE vm_alloc(VALUE r_self)
113
133
  {
114
134
  VMData *data;
@@ -118,12 +138,17 @@ static VALUE vm_alloc(VALUE r_self)
118
138
  data->alive_objects = rb_hash_new();
119
139
  data->module_loader = Qnil;
120
140
  data->j_file_proxy_creator = JS_UNDEFINED;
141
+ data->oom_poisoned = false;
121
142
 
122
143
  EvalTime *eval_time = malloc(sizeof(EvalTime));
123
144
  data->eval_time = eval_time;
124
145
 
125
- JSRuntime *runtime = JS_NewRuntime();
126
- 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;
127
152
 
128
153
  return obj;
129
154
  }