quickjs 0.16.0 → 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: e8f0513a5e0a54c24bdc8c20000f8cf9d1611e394683a710a2260ebedb57913a
4
- data.tar.gz: 335ab02e13a0b8b137342f5895bc821641ac33d6f8370e05b9c24f0ca3ce7416
3
+ metadata.gz: 7b233c0e106654a50f4af274fef6a4dd77f0b53cc0a703473cda844da6ba3763
4
+ data.tar.gz: 50dfeaf56422ee74e8cb8711a58e1678c8141ac4bb7256a4d8861fa1a9415c93
5
5
  SHA512:
6
- metadata.gz: f6e975069d7b77adf0d2e4ca7d6459c6499e1465155cae563ff3fb87d7f1f8aefe9f3b26191d9736a2c398ee13df48d5ded389b17f54aec56dc2bf4e4ac580a8
7
- data.tar.gz: b1341d5c2e0ba51e6891752bc6c203fe7c2bfc8e1d76beafaf93129f3fb0bb600c64e786beec42ec6a9cc95336562de739d32f02711422b1f9930dd4b57cbb93
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
  {
@@ -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);
@@ -755,6 +796,40 @@ static JSValue js_console_error(JSContext *ctx, JSValueConst this, int argc, JSV
755
796
  return js_quickjsrb_log(ctx, this, argc, argv, "error");
756
797
  }
757
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
+
758
833
  static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
759
834
  {
760
835
  VALUE r_opts;
@@ -820,15 +895,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
820
895
  JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), "<vm>", JS_EVAL_TYPE_GLOBAL);
821
896
  JS_FreeValue(data->context, j_defineIntl);
822
897
 
823
- JSValue j_polyfillIntlObject = JS_ReadObject(data->context, &qjsc_polyfill_intl_en_min, qjsc_polyfill_intl_en_min_size, JS_READ_OBJ_BYTECODE);
824
- 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);
825
899
  JS_FreeValue(data->context, j_polyfillIntlResult);
826
900
  }
827
901
 
828
902
  if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillFileId))))
829
903
  {
830
- JSValue j_polyfillFileObject = JS_ReadObject(data->context, &qjsc_polyfill_file_min, qjsc_polyfill_file_min_size, JS_READ_OBJ_BYTECODE);
831
- 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);
832
905
  JS_FreeValue(data->context, j_polyfillFileResult);
833
906
 
834
907
  quickjsrb_init_file_proxy(data);
@@ -836,15 +909,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
836
909
 
837
910
  if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillEncodingId))))
838
911
  {
839
- JSValue j_polyfillEncodingObject = JS_ReadObject(data->context, &qjsc_polyfill_encoding_min, qjsc_polyfill_encoding_min_size, JS_READ_OBJ_BYTECODE);
840
- 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);
841
913
  JS_FreeValue(data->context, j_polyfillEncodingResult);
842
914
  }
843
915
 
844
916
  if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillUrlId))))
845
917
  {
846
- JSValue j_polyfillUrlObject = JS_ReadObject(data->context, &qjsc_polyfill_url_min, qjsc_polyfill_url_min_size, JS_READ_OBJ_BYTECODE);
847
- 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);
848
919
  JS_FreeValue(data->context, j_polyfillUrlResult);
849
920
  }
850
921
 
@@ -853,6 +924,10 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
853
924
  quickjsrb_init_crypto(data->context, j_global);
854
925
  }
855
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.
856
931
  JSValue j_console = JS_NewObject(data->context);
857
932
  JS_SetPropertyStr(
858
933
  data->context, j_console, "log",
@@ -900,35 +975,69 @@ static VALUE to_rb_return_value(JSContext *ctx, JSValue j_val)
900
975
  return result;
901
976
  }
902
977
 
903
- static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
978
+ static void check_oom_poisoned(VMData *data)
904
979
  {
905
- VMData *data;
906
- TypedData_Get_Struct(r_self, VMData, &vm_type, data);
907
-
908
- VALUE r_code, r_opts;
909
- 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
+ }
910
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
+ {
911
992
  if (!RB_TYPE_P(r_code, T_STRING))
912
993
  {
913
994
  VALUE r_code_class = rb_class_name(CLASS_OF(r_code));
914
995
  rb_raise(rb_eTypeError, "JavaScript code must be a String, got %s", StringValueCStr(r_code_class));
915
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);
916
1022
 
917
- const char *filename = "<code>";
1023
+ bool async_mode = true;
918
1024
  if (!NIL_P(r_opts))
919
1025
  {
920
- VALUE r_filename = rb_hash_aref(r_opts, ID2SYM(rb_intern("filename")));
921
- if (!NIL_P(r_filename))
922
- {
923
- Check_Type(r_filename, T_STRING);
924
- filename = StringValueCStr(r_filename);
925
- }
1026
+ VALUE r_async = rb_hash_aref(r_opts, ID2SYM(rb_intern("async")));
1027
+ if (r_async == Qfalse)
1028
+ async_mode = false;
926
1029
  }
927
1030
 
928
- clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
929
- JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
1031
+ arm_eval_timer(data);
930
1032
 
931
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
+
932
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);
933
1042
  JSValue j_awaitedResult = js_std_await(data->context, j_codeResult); // This frees j_codeResult
934
1043
  // JS_EVAL_FLAG_ASYNC wraps the result in {value, done} — extract the actual value
@@ -938,6 +1047,74 @@ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
938
1047
  return to_rb_return_value(data->context, j_returnedValue);
939
1048
  }
940
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
+
941
1118
  static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
942
1119
  {
943
1120
  rb_need_block();
@@ -1070,6 +1247,8 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
1070
1247
  VMData *data;
1071
1248
  TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1072
1249
 
1250
+ check_oom_poisoned(data);
1251
+
1073
1252
  JSValue j_this = JS_UNDEFINED;
1074
1253
  JSValue j_func;
1075
1254
 
@@ -1297,11 +1476,53 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
1297
1476
  rb_define_alloc_func(r_class_vm, vm_alloc);
1298
1477
  rb_define_method(r_class_vm, "initialize", vm_m_initialize, -1);
1299
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);
1300
1481
  rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
1301
1482
  rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
1302
1483
  rb_define_method(r_class_vm, "import", vm_m_import, -1);
1303
1484
  rb_define_method(r_class_vm, "module_loader", vm_m_get_module_loader, 0);
1304
1485
  rb_define_method(r_class_vm, "module_loader=", vm_m_set_module_loader, 1);
1305
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);
1306
1490
  r_define_log_class(r_class_vm);
1307
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
  }