quickjs 0.20.0 → 0.21.0.rc1
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 +13 -0
- data/ext/quickjsrb/quickjsrb.c +608 -102
- data/ext/quickjsrb/quickjsrb.h +56 -0
- data/ext/quickjsrb/quickjsrb_crypto.c +2 -2
- data/ext/quickjsrb/quickjsrb_crypto_subtle.c +13 -12
- data/ext/quickjsrb/quickjsrb_file.c +7 -7
- data/lib/quickjs/polyfills.rb +30 -11
- data/lib/quickjs/subtle_crypto.rb +6 -0
- data/lib/quickjs/version.rb +1 -1
- data/polyfills/package-lock.json +2 -2
- data/polyfills/package.json +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b261a41c198fb0494661ceee781db96bdb3a03614dfd4e5cb4361e0346979df
|
|
4
|
+
data.tar.gz: d22a2d8524c572dc6ba0d58a25ef2efe1474bdfed6f623b4bfe54564f6ec4fd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 277439c6f9183e3d7cd72d4a468384adac3ef3c5a33df9d3db6ceb86139d0dae2d2e75591cc4240159d8a3bbd04db33cb7fa077a03dd70c6a38a8227078723c1
|
|
7
|
+
data.tar.gz: bbb5bb21051db7337cbb14ff449bcc649b48c783fa947edb6edd0ec2cb643b8cc925259dbe771ee4d9b7ba31fb98c889ce1a289b5e9cbdf36640678697af3c03
|
data/README.md
CHANGED
|
@@ -358,6 +358,8 @@ vm.eval_code('1 + 1') # raises Quickjs::RuntimeError "VM has been disposed"
|
|
|
358
358
|
Thread.new { vm.dispose! }
|
|
359
359
|
```
|
|
360
360
|
|
|
361
|
+
Disposing a VM that is mid-evaluation on another thread would free the runtime out from under the running JS, so `dispose!` raises `ThreadError` while JS is executing on the VM (`eval_code`, `call`, `import`, `drain_jobs!`, `Runnable#run`) — dispose after the call returns.
|
|
362
|
+
|
|
361
363
|
#### `Quickjs::VM#drain_jobs!`: Run pending JS jobs to completion
|
|
362
364
|
|
|
363
365
|
QuickJS does not automatically drain the job queue at the end of a synchronous `eval_code` / `call`. Continuations scheduled via `Promise.resolve().then(...)` or `JS_EnqueueJob` stay pending until something explicitly runs them — `await` inside JS does, but a sync return path does not.
|
|
@@ -374,6 +376,17 @@ vm.eval_code('x') #=> 1
|
|
|
374
376
|
|
|
375
377
|
Useful when porting JS that assumed V8's implicit-drain semantics — V8 (and therefore [mini_racer](https://github.com/rubyjs/mini_racer)) flushes pending jobs at every eval boundary, so `eval_code` already sees `.then()` continuations run by the time it returns. QuickJS doesn't. Patterns like `Promise.resolve().then(() => { ... })` and Stimulus/Hotwire callbacks that assume "the next microtask tick" silently fall through unless you call `drain_jobs!` explicitly.
|
|
376
378
|
|
|
379
|
+
#### Threads and parallelism
|
|
380
|
+
|
|
381
|
+
`eval_code` releases Ruby's GVL while JS runs, as long as no JS→Ruby bridge is registered on the VM (no `define_function`, `module_loader`, `on_unhandled_rejection`, and none of `FEATURE_TIMEOUT` / `POLYFILL_FILE` / `POLYFILL_CRYPTO` — `console.log` is fine). Separate VMs on separate Ruby threads then evaluate genuinely in parallel on multi-core hosts; when a bridge is registered, the GVL stays held for that VM's evals and they serialize as usual.
|
|
382
|
+
|
|
383
|
+
The rules for sharing VMs across threads:
|
|
384
|
+
|
|
385
|
+
- **One VM, one thread at a time.** A `Quickjs::VM` is not safe for concurrent use from multiple threads — QuickJS contexts have no internal locking. Handing a VM off between threads (e.g. constructing it on a warmer thread and using it on another) is fine as long as only one thread touches it at a time.
|
|
386
|
+
- **Create the VM on the thread that evaluates with it** when possible: QuickJS records the creating thread's stack bounds, and evaluating from a thread whose stack sits below them can trip a false stack-overflow error.
|
|
387
|
+
- **Register bridges before evaluating.** `define_function`, `module_loader=`, and `on_unhandled_rejection` raise `ThreadError` while a GVL-released eval is in flight (e.g. from inside an `on_log` listener) — the running JS was allowed to release the GVL precisely because no bridge existed when it started.
|
|
388
|
+
- **`MODULE_OS` caveat:** `os.signal` and `os.ttySetRaw` mutate process-wide state inside quickjs-libc, so don't call those two from VMs running concurrently on different threads. The common APIs (`os.sleep`, `os.setTimeout`, file I/O) only touch per-runtime state and are safe.
|
|
389
|
+
|
|
377
390
|
### Value Conversion
|
|
378
391
|
|
|
379
392
|
| JavaScript | | Ruby | Note |
|
data/ext/quickjsrb/quickjsrb.c
CHANGED
|
@@ -25,6 +25,8 @@ const char *native_errors[] = {
|
|
|
25
25
|
const int num_native_errors = sizeof(native_errors) / sizeof(native_errors[0]);
|
|
26
26
|
|
|
27
27
|
static int dispatch_log(VMData *data, const char *severity, VALUE r_row);
|
|
28
|
+
static void check_disposed(VMData *data);
|
|
29
|
+
static void run_gvl_release_region(VMData *data, void *(*job_run)(void *), void *job, JSValue *j_result, void *owned_buf0, void *owned_buf1);
|
|
28
30
|
|
|
29
31
|
JSValue to_js_value(JSContext *ctx, VALUE r_value);
|
|
30
32
|
VALUE to_rb_value(JSContext *ctx, JSValue j_val);
|
|
@@ -871,7 +873,10 @@ static JSValue js_quickjsrb_set_timeout(JSContext *ctx, JSValueConst _this, int
|
|
|
871
873
|
return JS_UNDEFINED;
|
|
872
874
|
}
|
|
873
875
|
|
|
874
|
-
|
|
876
|
+
// The single way the on_log listener is invoked. Called under rb_protect by
|
|
877
|
+
// dispatch_log (uncaught-error headline rows) and unprotected — the caller's
|
|
878
|
+
// outer rb_protect covers it — by r_build_and_dispatch_log (console rows).
|
|
879
|
+
static VALUE r_call_log_listener(VALUE r_args)
|
|
875
880
|
{
|
|
876
881
|
VALUE r_listener = RARRAY_AREF(r_args, 0);
|
|
877
882
|
VALUE r_log = RARRAY_AREF(r_args, 1);
|
|
@@ -886,7 +891,7 @@ static int dispatch_log(VMData *data, const char *severity, VALUE r_row)
|
|
|
886
891
|
VALUE r_log = r_log_new(severity, r_row);
|
|
887
892
|
VALUE r_args = rb_ary_new3(2, data->log_listener, r_log);
|
|
888
893
|
int error;
|
|
889
|
-
rb_protect(
|
|
894
|
+
rb_protect(r_call_log_listener, r_args, &error);
|
|
890
895
|
return error;
|
|
891
896
|
}
|
|
892
897
|
|
|
@@ -902,13 +907,30 @@ static VALUE vm_m_on_log(VALUE r_self)
|
|
|
902
907
|
return Qnil;
|
|
903
908
|
}
|
|
904
909
|
|
|
905
|
-
|
|
910
|
+
struct quickjsrb_log_call
|
|
906
911
|
{
|
|
912
|
+
JSContext *ctx;
|
|
913
|
+
int argc;
|
|
914
|
+
JSValueConst *argv;
|
|
915
|
+
const char *severity;
|
|
916
|
+
JSValue result;
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// Runs under rb_protect (see js_quickjsrb_log_inner) so no Ruby raise —
|
|
920
|
+
// to_rb_value on an unconvertible argument (e.g. a Promise nested inside an
|
|
921
|
+
// array), allocation failure, or the user's on_log listener — can longjmp
|
|
922
|
+
// through QuickJS's interpreter frames, or, on the pure path (where this
|
|
923
|
+
// runs inside rb_thread_call_with_gvl), across the rb_thread_call_without_gvl
|
|
924
|
+
// region — which would leak its buffers and leave gvl_released_js stuck.
|
|
925
|
+
static VALUE r_build_and_dispatch_log(VALUE r_call)
|
|
926
|
+
{
|
|
927
|
+
struct quickjsrb_log_call *call = (struct quickjsrb_log_call *)r_call;
|
|
928
|
+
JSContext *ctx = call->ctx;
|
|
907
929
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
908
930
|
VALUE r_row = rb_ary_new();
|
|
909
|
-
for (int i = 0; i < argc; i++)
|
|
931
|
+
for (int i = 0; i < call->argc; i++)
|
|
910
932
|
{
|
|
911
|
-
|
|
933
|
+
JSValueConst j_logged = call->argv[i];
|
|
912
934
|
VALUE r_raw;
|
|
913
935
|
if (JS_VALUE_GET_NORM_TAG(j_logged) == JS_TAG_OBJECT && JS_PromiseState(ctx, j_logged) != -1)
|
|
914
936
|
{
|
|
@@ -946,12 +968,22 @@ static JSValue js_quickjsrb_log(JSContext *ctx, JSValueConst _this, int argc, JS
|
|
|
946
968
|
const char *body = JS_ToCString(ctx, j_logged);
|
|
947
969
|
VALUE r_c = rb_str_new2(body);
|
|
948
970
|
JS_FreeCString(ctx, body);
|
|
949
|
-
JS_FreeValue(ctx, j_logged);
|
|
950
971
|
|
|
951
972
|
rb_ary_push(r_row, r_log_body_new(r_raw, r_c));
|
|
952
973
|
}
|
|
953
974
|
|
|
954
|
-
|
|
975
|
+
r_call_log_listener(rb_ary_new3(2, data->log_listener, r_log_new(call->severity, r_row)));
|
|
976
|
+
return Qnil;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Requires the GVL. A caught Ruby exception (from row building or the
|
|
980
|
+
// listener) becomes a JS throw, so it unwinds through QuickJS as a regular
|
|
981
|
+
// JS exception instead of a cross-boundary longjmp.
|
|
982
|
+
static JSValue js_quickjsrb_log_inner(JSContext *ctx, int argc, JSValueConst *argv, const char *severity)
|
|
983
|
+
{
|
|
984
|
+
struct quickjsrb_log_call call = {ctx, argc, argv, severity, JS_UNDEFINED};
|
|
985
|
+
int error;
|
|
986
|
+
rb_protect(r_build_and_dispatch_log, (VALUE)&call, &error);
|
|
955
987
|
if (error)
|
|
956
988
|
{
|
|
957
989
|
VALUE r_error = rb_errinfo();
|
|
@@ -962,37 +994,102 @@ static JSValue js_quickjsrb_log(JSContext *ctx, JSValueConst _this, int argc, JS
|
|
|
962
994
|
return JS_UNDEFINED;
|
|
963
995
|
}
|
|
964
996
|
|
|
997
|
+
static void *quickjsrb_log_with_gvl(void *p)
|
|
998
|
+
{
|
|
999
|
+
struct quickjsrb_log_call *c = p;
|
|
1000
|
+
VMData *data = JS_GetContextOpaque(c->ctx);
|
|
1001
|
+
// The GVL is held for the duration of this callback. Clear the flag so
|
|
1002
|
+
// JS re-entered from the on_log listener (a bridged eval_code, call,
|
|
1003
|
+
// eval_bytecode, ...) routes its console.log inline — with the flag
|
|
1004
|
+
// still true it would call rb_thread_call_with_gvl while already holding
|
|
1005
|
+
// the GVL, which MRI aborts on. A plain save/restore pair suffices: the
|
|
1006
|
+
// listener can't longjmp out (js_quickjsrb_log_inner rb_protects
|
|
1007
|
+
// everything it runs).
|
|
1008
|
+
bool prev = data->gvl_released_js;
|
|
1009
|
+
data->gvl_released_js = false;
|
|
1010
|
+
c->result = js_quickjsrb_log_inner(c->ctx, c->argc, c->argv, c->severity);
|
|
1011
|
+
data->gvl_released_js = prev;
|
|
1012
|
+
return NULL;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Dispatcher: when the caller released the GVL around JS_Eval (see
|
|
1016
|
+
// vm_m_evalCode pure-path), Ruby APIs can't be touched directly. Re-acquire
|
|
1017
|
+
// the GVL via rb_thread_call_with_gvl before running the row-building body.
|
|
1018
|
+
// When the GVL is already held, call the body inline to avoid the re-acquire
|
|
1019
|
+
// overhead.
|
|
1020
|
+
static JSValue js_quickjsrb_log(JSContext *ctx, int argc, JSValueConst *argv, const char *severity)
|
|
1021
|
+
{
|
|
1022
|
+
VMData *data = JS_GetContextOpaque(ctx);
|
|
1023
|
+
// With no listener registered the built row would be discarded, so skip
|
|
1024
|
+
// the whole pipeline — most importantly the GVL re-acquire below, which
|
|
1025
|
+
// would otherwise turn every console.log of a log-heavy pure-path script
|
|
1026
|
+
// into a full GVL round-trip for nothing. This deliberately also skips
|
|
1027
|
+
// argument stringification (getters / toString / to_rb_value) and its
|
|
1028
|
+
// side effects or failures: console.log with no listener is a true
|
|
1029
|
+
// no-op. Reading a VALUE field for a Qnil comparison is a plain aligned
|
|
1030
|
+
// pointer-sized read, safe without the GVL; a racing on_log registration
|
|
1031
|
+
// (which itself requires the GVL) at worst drops this one row.
|
|
1032
|
+
if (NIL_P(data->log_listener))
|
|
1033
|
+
return JS_UNDEFINED;
|
|
1034
|
+
if (data->gvl_released_js)
|
|
1035
|
+
{
|
|
1036
|
+
struct quickjsrb_log_call c = {ctx, argc, argv, severity, JS_UNDEFINED};
|
|
1037
|
+
rb_thread_call_with_gvl(quickjsrb_log_with_gvl, &c);
|
|
1038
|
+
return c.result;
|
|
1039
|
+
}
|
|
1040
|
+
return js_quickjsrb_log_inner(ctx, argc, argv, severity);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
965
1043
|
static JSValue js_console_info(JSContext *ctx, JSValueConst this, int argc, JSValueConst *argv)
|
|
966
1044
|
{
|
|
967
|
-
return js_quickjsrb_log(ctx,
|
|
1045
|
+
return js_quickjsrb_log(ctx, argc, argv, "info");
|
|
968
1046
|
}
|
|
969
1047
|
|
|
970
1048
|
static JSValue js_console_verbose(JSContext *ctx, JSValueConst this, int argc, JSValueConst *argv)
|
|
971
1049
|
{
|
|
972
|
-
return js_quickjsrb_log(ctx,
|
|
1050
|
+
return js_quickjsrb_log(ctx, argc, argv, "verbose");
|
|
973
1051
|
}
|
|
974
1052
|
|
|
975
1053
|
static JSValue js_console_warn(JSContext *ctx, JSValueConst this, int argc, JSValueConst *argv)
|
|
976
1054
|
{
|
|
977
|
-
return js_quickjsrb_log(ctx,
|
|
1055
|
+
return js_quickjsrb_log(ctx, argc, argv, "warning");
|
|
978
1056
|
}
|
|
979
1057
|
|
|
980
1058
|
static JSValue js_console_error(JSContext *ctx, JSValueConst this, int argc, JSValueConst *argv)
|
|
981
1059
|
{
|
|
982
|
-
return js_quickjsrb_log(ctx,
|
|
1060
|
+
return js_quickjsrb_log(ctx, argc, argv, "error");
|
|
983
1061
|
}
|
|
984
1062
|
|
|
985
1063
|
// Run polyfill bytecode load + eval without the GVL so a background
|
|
986
1064
|
// warmer thread can populate a VM pool in parallel with the main thread
|
|
987
1065
|
// on multi-core hosts.
|
|
988
1066
|
//
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
|
|
1067
|
+
// Two call sites use this helper:
|
|
1068
|
+
//
|
|
1069
|
+
// 1. vm_m_initialize — pre-built bytecode (file / encoding / url) embedded
|
|
1070
|
+
// as static C constants. setTimeout (FEATURE_TIMEOUT) and the File
|
|
1071
|
+
// proxy (POLYFILL_FILE, registered ahead of the encoding/url loads)
|
|
1072
|
+
// can already be live here, so the release is safe not because
|
|
1073
|
+
// nothing is registered, but because a load never runs bridge code:
|
|
1074
|
+
// js_quickjsrb_set_timeout only enqueues (pure C; the Ruby-calling
|
|
1075
|
+
// js_delay_and_eval_job runs later, under a GVL-held drain/await), a
|
|
1076
|
+
// load never drains the job queue, and the bundled polyfill
|
|
1077
|
+
// top-levels (built from polyfills/src in this repo) don't call the
|
|
1078
|
+
// File proxy. That audit is the invariant to preserve when rebuilding
|
|
1079
|
+
// bundles or reordering vm_m_initialize.
|
|
1080
|
+
//
|
|
1081
|
+
// 2. vm_m_loadPolyfillBytecode — bytecode from a Ruby String, copied to a
|
|
1082
|
+
// malloc'd buffer first so the buffer survives a GC compact. This
|
|
1083
|
+
// bytecode is arbitrary (registered by companion gems), so no audit
|
|
1084
|
+
// applies: the caller gates the release on can_eval_gvl_free() to
|
|
1085
|
+
// bail whenever a direct-rb_funcall bridge (File proxy, crypto, …)
|
|
1086
|
+
// is installed; console.log is covered by js_quickjsrb_log's
|
|
1087
|
+
// gvl_released_js re-acquire either way.
|
|
1088
|
+
//
|
|
1089
|
+
// load_polyfill_bytecode delegates to the shared GVL-release region (see
|
|
1090
|
+
// run_gvl_release_region) so every caller inherits the re-acquire safety,
|
|
1091
|
+
// the dispose! handshake, and interrupt-proof cleanup automatically.
|
|
1092
|
+
struct bytecode_load_job
|
|
996
1093
|
{
|
|
997
1094
|
JSContext *ctx;
|
|
998
1095
|
const uint8_t *buf;
|
|
@@ -1000,19 +1097,33 @@ struct polyfill_load_args
|
|
|
1000
1097
|
JSValue result;
|
|
1001
1098
|
};
|
|
1002
1099
|
|
|
1003
|
-
|
|
1100
|
+
// Shared bytecode read + eval core — pure C over JSValues, MUST NOT touch
|
|
1101
|
+
// the Ruby VM (the polyfill release path runs it without the GVL; the
|
|
1102
|
+
// GVL-held call sites invoke it directly).
|
|
1103
|
+
static void *bytecode_load_job_run(void *p)
|
|
1004
1104
|
{
|
|
1005
|
-
struct
|
|
1006
|
-
JSValue obj = JS_ReadObject(
|
|
1007
|
-
|
|
1105
|
+
struct bytecode_load_job *job = p;
|
|
1106
|
+
JSValue obj = JS_ReadObject(job->ctx, job->buf, job->buf_len, JS_READ_OBJ_BYTECODE);
|
|
1107
|
+
// JS_EvalFunction on a JS_EXCEPTION input replaces the pending exception
|
|
1108
|
+
// with a generic "bytecode function expected" TypeError, losing the actual
|
|
1109
|
+
// deserialization diagnostic from JS_ReadObject. Short-circuit instead.
|
|
1110
|
+
if (JS_IsException(obj))
|
|
1111
|
+
{
|
|
1112
|
+
job->result = obj;
|
|
1113
|
+
return NULL;
|
|
1114
|
+
}
|
|
1115
|
+
job->result = JS_EvalFunction(job->ctx, obj); // frees obj
|
|
1008
1116
|
return NULL;
|
|
1009
1117
|
}
|
|
1010
1118
|
|
|
1011
|
-
|
|
1119
|
+
// take_ownership: true when buf is malloc'd storage that the release region
|
|
1120
|
+
// should free on every exit path (including async-interrupt unwinds); false
|
|
1121
|
+
// when buf points at static bytecode.
|
|
1122
|
+
static JSValue load_polyfill_bytecode(VMData *data, const uint8_t *buf, size_t buf_len, bool take_ownership)
|
|
1012
1123
|
{
|
|
1013
|
-
struct
|
|
1014
|
-
|
|
1015
|
-
return
|
|
1124
|
+
struct bytecode_load_job job = {data->context, buf, buf_len, JS_UNDEFINED};
|
|
1125
|
+
run_gvl_release_region(data, bytecode_load_job_run, &job, &job.result, take_ownership ? (uint8_t *)buf : NULL, NULL);
|
|
1126
|
+
return job.result;
|
|
1016
1127
|
}
|
|
1017
1128
|
|
|
1018
1129
|
static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
@@ -1070,14 +1181,18 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
1070
1181
|
}
|
|
1071
1182
|
else if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featureTimeoutId))))
|
|
1072
1183
|
{
|
|
1184
|
+
// setTimeout itself only enqueues, but js_delay_and_eval_job (the
|
|
1185
|
+
// enqueued callback) calls rb_funcall and rb_thread_wait_for
|
|
1186
|
+
// synchronously while js_std_await drains the queue — so this counts
|
|
1187
|
+
// as a Ruby bridge and eval must keep the GVL.
|
|
1073
1188
|
JS_SetPropertyStr(
|
|
1074
1189
|
data->context, j_global, "setTimeout",
|
|
1075
|
-
|
|
1190
|
+
quickjsrb_new_ruby_bridge(data->context, js_quickjsrb_set_timeout, "setTimeout", 2));
|
|
1076
1191
|
}
|
|
1077
1192
|
|
|
1078
1193
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillFileId))))
|
|
1079
1194
|
{
|
|
1080
|
-
JSValue j_polyfillFileResult = load_polyfill_bytecode(data
|
|
1195
|
+
JSValue j_polyfillFileResult = load_polyfill_bytecode(data, &qjsc_polyfill_file_min, qjsc_polyfill_file_min_size, false);
|
|
1081
1196
|
JS_FreeValue(data->context, j_polyfillFileResult);
|
|
1082
1197
|
|
|
1083
1198
|
quickjsrb_init_file_proxy(data);
|
|
@@ -1085,13 +1200,13 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
1085
1200
|
|
|
1086
1201
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillEncodingId))))
|
|
1087
1202
|
{
|
|
1088
|
-
JSValue j_polyfillEncodingResult = load_polyfill_bytecode(data
|
|
1203
|
+
JSValue j_polyfillEncodingResult = load_polyfill_bytecode(data, &qjsc_polyfill_encoding_min, qjsc_polyfill_encoding_min_size, false);
|
|
1089
1204
|
JS_FreeValue(data->context, j_polyfillEncodingResult);
|
|
1090
1205
|
}
|
|
1091
1206
|
|
|
1092
1207
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillUrlId))))
|
|
1093
1208
|
{
|
|
1094
|
-
JSValue j_polyfillUrlResult = load_polyfill_bytecode(data
|
|
1209
|
+
JSValue j_polyfillUrlResult = load_polyfill_bytecode(data, &qjsc_polyfill_url_min, qjsc_polyfill_url_min_size, false);
|
|
1095
1210
|
JS_FreeValue(data->context, j_polyfillUrlResult);
|
|
1096
1211
|
}
|
|
1097
1212
|
|
|
@@ -1100,10 +1215,11 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
1100
1215
|
quickjsrb_init_crypto(data->context, j_global);
|
|
1101
1216
|
}
|
|
1102
1217
|
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1218
|
+
// console and the remaining host callbacks are registered below this
|
|
1219
|
+
// point. setTimeout and the File proxy above predate the GVL-released
|
|
1220
|
+
// polyfill loads only under the audit described at
|
|
1221
|
+
// load_polyfill_bytecode — re-read it before reordering this function
|
|
1222
|
+
// or registering anything else above the loads.
|
|
1107
1223
|
JSValue j_console = JS_NewObject(data->context);
|
|
1108
1224
|
JS_SetPropertyStr(
|
|
1109
1225
|
data->context, j_console, "log",
|
|
@@ -1194,6 +1310,266 @@ static void arm_eval_timer(VMData *data)
|
|
|
1194
1310
|
JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
|
|
1195
1311
|
}
|
|
1196
1312
|
|
|
1313
|
+
// Pure-path predicate: true when no JS→Ruby bridge can fire during eval
|
|
1314
|
+
// other than console.log (which is handled by js_quickjsrb_log's
|
|
1315
|
+
// gvl_released_js re-acquire). When true, eval can safely run with the
|
|
1316
|
+
// GVL released so other Ruby threads make progress on different cores.
|
|
1317
|
+
// C-function bridges registered via quickjsrb_new_ruby_bridge (crypto.*,
|
|
1318
|
+
// File proxy, setTimeout) call rb_funcall directly — those would need to
|
|
1319
|
+
// learn the gvl_released_js pattern before they can run under a released
|
|
1320
|
+
// GVL, so we hold the GVL when any of them is installed.
|
|
1321
|
+
//
|
|
1322
|
+
// :feature_std / :feature_os intentionally pass: quickjs-libc never calls
|
|
1323
|
+
// into Ruby, and its state is almost entirely per-runtime (JSThreadState).
|
|
1324
|
+
// The exceptions are two file-scope globals reachable from niche APIs —
|
|
1325
|
+
// `oldtty` (os.ttySetRaw) and `os_pending_signals` (os.signal) — which can
|
|
1326
|
+
// race when multiple VMs run those APIs concurrently. Gating the whole
|
|
1327
|
+
// feature would re-serialize os.sleep / os.setTimeout across threads, so
|
|
1328
|
+
// the constraint is documented in the README instead.
|
|
1329
|
+
static bool can_eval_gvl_free(VMData *data)
|
|
1330
|
+
{
|
|
1331
|
+
return RHASH_SIZE(data->defined_functions) == 0
|
|
1332
|
+
&& NIL_P(data->module_loader)
|
|
1333
|
+
&& NIL_P(data->on_unhandled_rejection)
|
|
1334
|
+
&& !data->has_native_ruby_bridge;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
struct eval_code_job
|
|
1338
|
+
{
|
|
1339
|
+
JSContext *ctx;
|
|
1340
|
+
const char *code;
|
|
1341
|
+
size_t code_len;
|
|
1342
|
+
const char *filename;
|
|
1343
|
+
bool async_mode;
|
|
1344
|
+
JSValue result;
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
// Shared eval core: JS_Eval (+ js_std_await and the {value, done} unwrap for
|
|
1348
|
+
// async). Pure C over JSValues — MUST NOT touch the Ruby VM, because the
|
|
1349
|
+
// pure path runs it with the GVL released (see js_quickjsrb_log's dispatcher
|
|
1350
|
+
// for how console.log re-acquires). The bridged path calls it directly with
|
|
1351
|
+
// the GVL held; both paths share this body so a future fix to the eval
|
|
1352
|
+
// sequence can't silently diverge between them.
|
|
1353
|
+
static void *eval_code_job_run(void *p)
|
|
1354
|
+
{
|
|
1355
|
+
struct eval_code_job *job = p;
|
|
1356
|
+
int eval_flags = job->async_mode ? (JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC) : JS_EVAL_TYPE_GLOBAL;
|
|
1357
|
+
JSValue j_codeResult = JS_Eval(job->ctx, job->code, job->code_len, job->filename, eval_flags);
|
|
1358
|
+
if (job->async_mode)
|
|
1359
|
+
{
|
|
1360
|
+
JSValue j_awaitedResult = js_std_await(job->ctx, j_codeResult); // frees j_codeResult
|
|
1361
|
+
job->result = JS_GetPropertyStr(job->ctx, j_awaitedResult, "value");
|
|
1362
|
+
JS_FreeValue(job->ctx, j_awaitedResult);
|
|
1363
|
+
}
|
|
1364
|
+
else
|
|
1365
|
+
{
|
|
1366
|
+
job->result = j_codeResult;
|
|
1367
|
+
}
|
|
1368
|
+
return NULL;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// A GVL-release region — the one place that owns the handshake required to
|
|
1372
|
+
// run a pure-C QuickJS job with the GVL released: the save/restore of
|
|
1373
|
+
// gvl_released_js (restore, not clear, so a region nested through an
|
|
1374
|
+
// on_log listener doesn't flip the outer region's console.log back to the
|
|
1375
|
+
// inline path), the evals_in_flight increment that makes dispose! refuse,
|
|
1376
|
+
// the stale-dispose re-check, and an rb_ensure cleanup that keeps all of
|
|
1377
|
+
// it balanced when an async interrupt (Thread#raise / Thread#kill /
|
|
1378
|
+
// Timeout) fires in rb_thread_call_without_gvl's GVL-re-acquire epilogue.
|
|
1379
|
+
struct gvl_release_region
|
|
1380
|
+
{
|
|
1381
|
+
VMData *data;
|
|
1382
|
+
void *(*job_run)(void *); // pure C over JSValues — must not touch Ruby
|
|
1383
|
+
void *job;
|
|
1384
|
+
// Where the job stores its JSValue result; the cleanup frees it when an
|
|
1385
|
+
// interrupt lands inside the region (no-op for JS_UNDEFINED).
|
|
1386
|
+
JSValue *j_result;
|
|
1387
|
+
// malloc'd inputs owned by the region so they survive an interrupt
|
|
1388
|
+
// unwinding past the caller's own frees; NULL slots are fine.
|
|
1389
|
+
void *owned_bufs[2];
|
|
1390
|
+
bool prev_gvl_released;
|
|
1391
|
+
bool completed;
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
static VALUE gvl_release_region_run(VALUE p)
|
|
1395
|
+
{
|
|
1396
|
+
struct gvl_release_region *region = (struct gvl_release_region *)p;
|
|
1397
|
+
rb_thread_call_without_gvl(region->job_run, region->job, NULL, NULL);
|
|
1398
|
+
region->completed = true;
|
|
1399
|
+
return Qnil;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
static VALUE gvl_release_region_cleanup(VALUE p)
|
|
1403
|
+
{
|
|
1404
|
+
struct gvl_release_region *region = (struct gvl_release_region *)p;
|
|
1405
|
+
VMData *data = region->data;
|
|
1406
|
+
data->gvl_released_js = region->prev_gvl_released;
|
|
1407
|
+
data->evals_in_flight--;
|
|
1408
|
+
data->gvl_release_regions--;
|
|
1409
|
+
free(region->owned_bufs[0]);
|
|
1410
|
+
free(region->owned_bufs[1]);
|
|
1411
|
+
// Frees the result when the interrupt landed after the job ran but
|
|
1412
|
+
// before the run function marked completion. An interrupt during the
|
|
1413
|
+
// caller's subsequent Ruby conversion can still leak the result — the
|
|
1414
|
+
// same (accepted) exposure every GVL-held path has always had.
|
|
1415
|
+
if (!region->completed)
|
|
1416
|
+
JS_FreeValue(data->context, *region->j_result);
|
|
1417
|
+
return Qnil;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Run job_run(job) with the GVL released. owned_buf0/1 are malloc'd
|
|
1421
|
+
// buffers backing the job's inputs; ownership transfers to the region,
|
|
1422
|
+
// which frees them on every exit path — including the disposed bail-out
|
|
1423
|
+
// below and async-interrupt unwinds. The caller's check_disposed may be
|
|
1424
|
+
// stale by now (argument parsing can yield the GVL to a concurrent
|
|
1425
|
+
// dispose!), so re-check here: nothing between this check and the release
|
|
1426
|
+
// yields, and dispose! refuses while evals_in_flight > 0, so the two
|
|
1427
|
+
// sides can't miss each other.
|
|
1428
|
+
static void run_gvl_release_region(VMData *data, void *(*job_run)(void *), void *job, JSValue *j_result, void *owned_buf0, void *owned_buf1)
|
|
1429
|
+
{
|
|
1430
|
+
if (data->disposed)
|
|
1431
|
+
{
|
|
1432
|
+
free(owned_buf0);
|
|
1433
|
+
free(owned_buf1);
|
|
1434
|
+
check_disposed(data); // raises
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
struct gvl_release_region region = {
|
|
1438
|
+
.data = data,
|
|
1439
|
+
.job_run = job_run,
|
|
1440
|
+
.job = job,
|
|
1441
|
+
.j_result = j_result,
|
|
1442
|
+
.owned_bufs = {owned_buf0, owned_buf1},
|
|
1443
|
+
.prev_gvl_released = data->gvl_released_js,
|
|
1444
|
+
.completed = false,
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
data->evals_in_flight++;
|
|
1448
|
+
data->gvl_release_regions++;
|
|
1449
|
+
data->gvl_released_js = true;
|
|
1450
|
+
rb_ensure(gvl_release_region_run, (VALUE)®ion, gvl_release_region_cleanup, (VALUE)®ion);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Installing a JS→Ruby bridge (define_function, module_loader=,
|
|
1454
|
+
// on_unhandled_rejection) invalidates the can_eval_gvl_free decision an
|
|
1455
|
+
// in-flight GVL-released eval was started under: after e.g. an on_log
|
|
1456
|
+
// listener returns, the still-running JS could reach the new bridge and
|
|
1457
|
+
// call Ruby APIs without holding the GVL. Refuse loudly — register
|
|
1458
|
+
// bridges before evaluating. Registration during GVL-held evals stays
|
|
1459
|
+
// allowed, as it always was: gvl_release_regions only counts released
|
|
1460
|
+
// regions.
|
|
1461
|
+
static void check_no_gvl_release_in_flight(VMData *data)
|
|
1462
|
+
{
|
|
1463
|
+
if (data->gvl_release_regions > 0)
|
|
1464
|
+
rb_raise(rb_eThreadError, "cannot install a JS-to-Ruby bridge on a Quickjs::VM while it is evaluating with the GVL released");
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
static VALUE evals_in_flight_release(VALUE p)
|
|
1468
|
+
{
|
|
1469
|
+
((VMData *)p)->evals_in_flight--;
|
|
1470
|
+
return Qnil;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Counterpart of run_gvl_release_region for the GVL-held entry points:
|
|
1474
|
+
// bridge callbacks (define_function procs, setTimeout's rb_thread_wait_for,
|
|
1475
|
+
// on_log listeners) yield the GVL mid-execution, so every JS execution must
|
|
1476
|
+
// elevate evals_in_flight for dispose! to refuse — and the decrement must
|
|
1477
|
+
// survive every raise exit: JS exceptions, type errors, conversion
|
|
1478
|
+
// failures, and async interrupts (Thread#raise / Timeout) delivered inside
|
|
1479
|
+
// a bridge callback. A stranded counter would make dispose! refuse forever,
|
|
1480
|
+
// so the check + increment + rb_ensure discipline lives here structurally
|
|
1481
|
+
// rather than being repeated at each call site. Entry points may have run
|
|
1482
|
+
// argument parsing that yields the GVL since their own entry check, hence
|
|
1483
|
+
// the disposed re-check immediately before counting. (The interrupt longjmp
|
|
1484
|
+
// still rips through QuickJS frames — a pre-existing hazard; whether the
|
|
1485
|
+
// bridge should rb_protect and convert instead is a semantics decision
|
|
1486
|
+
// deferred in the #56 review.)
|
|
1487
|
+
static VALUE run_held_js_entry(VMData *data, VALUE (*body)(VALUE), VALUE arg)
|
|
1488
|
+
{
|
|
1489
|
+
check_disposed(data);
|
|
1490
|
+
data->evals_in_flight++;
|
|
1491
|
+
return rb_ensure(body, arg, evals_in_flight_release, (VALUE)data);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
static VALUE eval_code_job_run_body(VALUE p)
|
|
1495
|
+
{
|
|
1496
|
+
eval_code_job_run((struct eval_code_job *)p);
|
|
1497
|
+
return Qnil;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// rb_ensure bodies over the shared bytecode core, for the GVL-held call
|
|
1501
|
+
// sites: vm_m_loadPolyfillBytecode's held fallback loads without awaiting
|
|
1502
|
+
// (matching its release path), vm_m_evalBytecode awaits the result. These
|
|
1503
|
+
// can take RSTRING_PTR without a copy: JS_ReadObject consumes the buffer
|
|
1504
|
+
// before any bridge can fire, so the GVL is provably held (no compaction)
|
|
1505
|
+
// for as long as the pointer is read.
|
|
1506
|
+
static VALUE bytecode_load_body(VALUE p)
|
|
1507
|
+
{
|
|
1508
|
+
bytecode_load_job_run((struct bytecode_load_job *)p);
|
|
1509
|
+
return Qnil;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
static VALUE bytecode_eval_await_body(VALUE p)
|
|
1513
|
+
{
|
|
1514
|
+
struct bytecode_load_job *job = (struct bytecode_load_job *)p;
|
|
1515
|
+
bytecode_load_job_run(job);
|
|
1516
|
+
// js_std_await passes a non-promise — including an exception preserved by
|
|
1517
|
+
// the JS_ReadObject short-circuit — through untouched.
|
|
1518
|
+
job->result = js_std_await(job->ctx, job->result);
|
|
1519
|
+
return Qnil;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Copy a Ruby String to a malloc'd buffer that outlives a GVL release —
|
|
1523
|
+
// RSTRING_PTR is GC-movable, so a released region must not read the
|
|
1524
|
+
// String's own storage. Ownership passes to the caller (the release
|
|
1525
|
+
// regions free it on every exit path). nul_terminate preserves the
|
|
1526
|
+
// sentinel NUL QuickJS's parser reads past the length: Ruby strings carry
|
|
1527
|
+
// one past RSTRING_LEN, so GVL-held paths get it for free, and a copy
|
|
1528
|
+
// must add it back. Allocates at least one byte either way — malloc(0)
|
|
1529
|
+
// may return NULL on success, and an empty String (e.g. empty bytecode)
|
|
1530
|
+
// must still reach the JS-level diagnostic instead of a spurious
|
|
1531
|
+
// NoMemError.
|
|
1532
|
+
static char *copy_rstring_to_owned_buffer(VALUE r_str, size_t *out_len, bool nul_terminate)
|
|
1533
|
+
{
|
|
1534
|
+
size_t len = (size_t)RSTRING_LEN(r_str);
|
|
1535
|
+
char *buf = malloc(nul_terminate ? len + 1 : (len > 0 ? len : 1));
|
|
1536
|
+
if (buf == NULL)
|
|
1537
|
+
rb_raise(rb_eNoMemError, "failed to allocate a GVL-release copy of a Ruby String");
|
|
1538
|
+
memcpy(buf, RSTRING_PTR(r_str), len);
|
|
1539
|
+
if (nul_terminate)
|
|
1540
|
+
buf[len] = '\0';
|
|
1541
|
+
*out_len = len;
|
|
1542
|
+
return buf;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Run the eval core without the GVL. Inputs are copied to malloc'd buffers
|
|
1546
|
+
// because RSTRING_PTR can be invalidated by GC compaction while we're
|
|
1547
|
+
// released.
|
|
1548
|
+
static VALUE eval_code_release_gvl(VMData *data, VALUE r_code, const char *filename, bool async_mode)
|
|
1549
|
+
{
|
|
1550
|
+
size_t code_len;
|
|
1551
|
+
char *code_buf = copy_rstring_to_owned_buffer(r_code, &code_len, true);
|
|
1552
|
+
|
|
1553
|
+
char *filename_buf = strdup(filename);
|
|
1554
|
+
if (filename_buf == NULL)
|
|
1555
|
+
{
|
|
1556
|
+
free(code_buf);
|
|
1557
|
+
rb_raise(rb_eNoMemError, "failed to allocate eval filename buffer");
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
struct eval_code_job job = {
|
|
1561
|
+
.ctx = data->context,
|
|
1562
|
+
.code = code_buf,
|
|
1563
|
+
.code_len = code_len,
|
|
1564
|
+
.filename = filename_buf,
|
|
1565
|
+
.async_mode = async_mode,
|
|
1566
|
+
.result = JS_UNDEFINED,
|
|
1567
|
+
};
|
|
1568
|
+
run_gvl_release_region(data, eval_code_job_run, &job, &job.result, code_buf, filename_buf);
|
|
1569
|
+
|
|
1570
|
+
return to_rb_return_value(data->context, job.result);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1197
1573
|
static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
|
|
1198
1574
|
{
|
|
1199
1575
|
VMData *data;
|
|
@@ -1218,19 +1594,23 @@ static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
|
|
|
1218
1594
|
|
|
1219
1595
|
StringValue(r_code);
|
|
1220
1596
|
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1597
|
+
if (can_eval_gvl_free(data))
|
|
1598
|
+
return eval_code_release_gvl(data, r_code, filename, async_mode);
|
|
1599
|
+
|
|
1600
|
+
// Bridged path: a JS→Ruby bridge (define_function / module loader /
|
|
1601
|
+
// setTimeout / File / crypto) may fire mid-eval, so keep the GVL held and
|
|
1602
|
+
// run the shared eval core directly. With the GVL held there's no
|
|
1603
|
+
// compaction risk, so RSTRING_PTR is usable without a malloc'd copy.
|
|
1604
|
+
struct eval_code_job job = {
|
|
1605
|
+
.ctx = data->context,
|
|
1606
|
+
.code = RSTRING_PTR(r_code),
|
|
1607
|
+
.code_len = (size_t)RSTRING_LEN(r_code),
|
|
1608
|
+
.filename = filename,
|
|
1609
|
+
.async_mode = async_mode,
|
|
1610
|
+
.result = JS_UNDEFINED,
|
|
1611
|
+
};
|
|
1612
|
+
run_held_js_entry(data, eval_code_job_run_body, (VALUE)&job);
|
|
1613
|
+
return to_rb_return_value(data->context, job.result);
|
|
1234
1614
|
}
|
|
1235
1615
|
|
|
1236
1616
|
static VALUE vm_m_compile(int argc, VALUE *argv, VALUE r_self)
|
|
@@ -1275,6 +1655,7 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1275
1655
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1276
1656
|
|
|
1277
1657
|
check_disposed(data);
|
|
1658
|
+
check_oom_poisoned(data);
|
|
1278
1659
|
|
|
1279
1660
|
if (!RB_TYPE_P(r_bytecode, T_STRING))
|
|
1280
1661
|
{
|
|
@@ -1286,22 +1667,21 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1286
1667
|
|
|
1287
1668
|
arm_eval_timer(data);
|
|
1288
1669
|
|
|
1289
|
-
// GVL
|
|
1290
|
-
//
|
|
1291
|
-
//
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
JSValue
|
|
1303
|
-
|
|
1304
|
-
JS_FreeValue(data->context, j_awaitedResult);
|
|
1670
|
+
// GVL held: user bytecode may invoke Ruby-bridged callbacks registered
|
|
1671
|
+
// via define_function, which call Ruby APIs. A pure VM could release
|
|
1672
|
+
// here with the same can_eval_gvl_free gate + buffer copy eval_code
|
|
1673
|
+
// uses — left GVL-held deliberately until bytecode eval shows up as a
|
|
1674
|
+
// parallelism bottleneck.
|
|
1675
|
+
struct bytecode_load_job job = {data->context,
|
|
1676
|
+
(const uint8_t *)RSTRING_PTR(r_bytecode),
|
|
1677
|
+
(size_t)RSTRING_LEN(r_bytecode),
|
|
1678
|
+
JS_UNDEFINED};
|
|
1679
|
+
run_held_js_entry(data, bytecode_eval_await_body, (VALUE)&job);
|
|
1680
|
+
if (JS_IsException(job.result))
|
|
1681
|
+
return to_rb_value(data->context, job.result); // raises
|
|
1682
|
+
|
|
1683
|
+
JSValue j_returnedValue = JS_GetPropertyStr(data->context, job.result, "value");
|
|
1684
|
+
JS_FreeValue(data->context, job.result);
|
|
1305
1685
|
return to_rb_return_value(data->context, j_returnedValue);
|
|
1306
1686
|
}
|
|
1307
1687
|
|
|
@@ -1309,29 +1689,87 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1309
1689
|
// The user's `timeout_msec` is a budget for *their* code; running a
|
|
1310
1690
|
// multi-MB polyfill bundle (e.g. the companion `quickjs-polyfill-intl`
|
|
1311
1691
|
// gem registered via Quickjs.register_polyfill) under that budget would
|
|
1312
|
-
// interrupt the load on tight defaults.
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1315
|
-
//
|
|
1316
|
-
//
|
|
1692
|
+
// interrupt the load on tight defaults.
|
|
1693
|
+
//
|
|
1694
|
+
// Picks the GVL-released or GVL-held path based on can_eval_gvl_free
|
|
1695
|
+
// (same gate vm_m_evalCode uses): if POLYFILL_FILE / POLYFILL_CRYPTO is
|
|
1696
|
+
// enabled, the polyfill's top-level code could reach a Ruby-bridged C
|
|
1697
|
+
// function that calls rb_funcall directly — those bridges don't honor
|
|
1698
|
+
// gvl_released_js, so we must keep the GVL. Otherwise, csim and similar
|
|
1699
|
+
// warmer-thread VM pools recover multi-core scaling: without the release
|
|
1700
|
+
// every warmer's polyfill load serializes through the GVL, collapsing
|
|
1701
|
+
// 4-way warmer parallelism to 1.
|
|
1702
|
+
//
|
|
1703
|
+
// The GVL-released path copies the Ruby String to a malloc'd buffer
|
|
1704
|
+
// because RSTRING_PTR can be invalidated by GC compaction while the GVL
|
|
1705
|
+
// is released. The memcpy cost is microseconds vs hundreds of ms of
|
|
1706
|
+
// bytecode eval, so it's negligible.
|
|
1317
1707
|
static VALUE vm_m_loadPolyfillBytecode(VALUE r_self, VALUE r_bytecode)
|
|
1318
1708
|
{
|
|
1319
1709
|
VMData *data;
|
|
1320
1710
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1321
1711
|
|
|
1322
|
-
|
|
1712
|
+
// StringValue can invoke a non-String argument's to_str, which may
|
|
1713
|
+
// yield the GVL — run it before the disposed check so nothing below
|
|
1714
|
+
// touches a context freed by a concurrent dispose! in that window.
|
|
1323
1715
|
StringValue(r_bytecode);
|
|
1324
1716
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1717
|
+
check_disposed(data);
|
|
1718
|
+
check_oom_poisoned(data);
|
|
1719
|
+
|
|
1720
|
+
// "Unbudgeted" needs enforcing, not just skipping arm_eval_timer: the
|
|
1721
|
+
// interrupt handler installed by a previous eval stays armed with that
|
|
1722
|
+
// eval's started_at, so a load after the budget lapsed would be
|
|
1723
|
+
// interrupted on its first check — and because the bytecode is
|
|
1724
|
+
// async-wrapped, the interruption surfaces as a rejected promise the
|
|
1725
|
+
// old code never looked at: the load "succeeded" with the polyfill
|
|
1726
|
+
// silently missing. Disarm for the load; the next eval re-arms. Only
|
|
1727
|
+
// when this load is the outermost JS activity, though: a load issued
|
|
1728
|
+
// from inside a bridge callback (e.g. an on_log listener) must stay
|
|
1729
|
+
// under the in-flight eval's budget, not erase it.
|
|
1730
|
+
if (data->evals_in_flight == 0)
|
|
1731
|
+
JS_SetInterruptHandler(JS_GetRuntime(data->context), NULL, NULL);
|
|
1732
|
+
|
|
1733
|
+
size_t buf_len = (size_t)RSTRING_LEN(r_bytecode);
|
|
1734
|
+
JSValue j_result;
|
|
1735
|
+
if (can_eval_gvl_free(data))
|
|
1736
|
+
{
|
|
1737
|
+
uint8_t *buf = (uint8_t *)copy_rstring_to_owned_buffer(r_bytecode, &buf_len, false);
|
|
1738
|
+
j_result = load_polyfill_bytecode(data, buf, buf_len, true);
|
|
1739
|
+
}
|
|
1740
|
+
else
|
|
1741
|
+
{
|
|
1742
|
+
// This branch runs exactly when a Ruby bridge (crypto, File, …) is
|
|
1743
|
+
// installed, and every rb_funcall inside one is an interrupt
|
|
1744
|
+
// checkpoint — run_held_js_entry keeps the counter unwind (and the
|
|
1745
|
+
// stale-dispose re-check the released branch gets from its region)
|
|
1746
|
+
// on this path too.
|
|
1747
|
+
struct bytecode_load_job job = {data->context,
|
|
1748
|
+
(const uint8_t *)RSTRING_PTR(r_bytecode),
|
|
1749
|
+
buf_len,
|
|
1750
|
+
JS_UNDEFINED};
|
|
1751
|
+
run_held_js_entry(data, bytecode_load_body, (VALUE)&job);
|
|
1752
|
+
j_result = job.result;
|
|
1753
|
+
}
|
|
1331
1754
|
|
|
1332
|
-
JSValue j_result = JS_EvalFunction(data->context, j_func); // frees j_func
|
|
1333
1755
|
if (JS_IsException(j_result))
|
|
1334
1756
|
return to_rb_value(data->context, j_result); // raises
|
|
1757
|
+
|
|
1758
|
+
// The compiled bytecode is async-wrapped (JS_EVAL_FLAG_ASYNC), so a
|
|
1759
|
+
// top-level throw doesn't come back as JS_EXCEPTION — it comes back as
|
|
1760
|
+
// a rejected promise. Surface it instead of silently shipping a VM
|
|
1761
|
+
// whose polyfill half-ran. Re-throwing the reason routes it through
|
|
1762
|
+
// to_rb_value's standard exception path, so interrupted/OOM mapping,
|
|
1763
|
+
// oom_poisoned latching, and the on_log error dispatch all behave
|
|
1764
|
+
// exactly as for a synchronous throw.
|
|
1765
|
+
if (JS_PromiseState(data->context, j_result) == JS_PROMISE_REJECTED)
|
|
1766
|
+
{
|
|
1767
|
+
JSValue j_reason = JS_PromiseResult(data->context, j_result);
|
|
1768
|
+
JS_FreeValue(data->context, j_result);
|
|
1769
|
+
JS_Throw(data->context, j_reason); // consumes j_reason
|
|
1770
|
+
return to_rb_value(data->context, JS_EXCEPTION); // raises
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1335
1773
|
JS_FreeValue(data->context, j_result);
|
|
1336
1774
|
return Qnil;
|
|
1337
1775
|
}
|
|
@@ -1349,6 +1787,7 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1349
1787
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1350
1788
|
|
|
1351
1789
|
check_disposed(data);
|
|
1790
|
+
check_no_gvl_release_in_flight(data);
|
|
1352
1791
|
|
|
1353
1792
|
if (RB_TYPE_P(r_name, T_ARRAY))
|
|
1354
1793
|
{
|
|
@@ -1393,6 +1832,10 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1393
1832
|
{
|
|
1394
1833
|
VALUE r_first_str = rb_funcall(RARRAY_AREF(r_name, 0), rb_intern("to_s"), 0);
|
|
1395
1834
|
const char *first_seg = StringValueCStr(r_first_str);
|
|
1835
|
+
// This lookup eval runs under whatever interrupt handler the
|
|
1836
|
+
// previous eval left armed; refresh the clock so a lapsed budget
|
|
1837
|
+
// can't misfire here and masquerade as "'%s' is not an object".
|
|
1838
|
+
arm_eval_timer(data);
|
|
1396
1839
|
j_parent = JS_Eval(data->context, first_seg, strlen(first_seg), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
|
|
1397
1840
|
|
|
1398
1841
|
if (JS_IsException(j_parent) || !JS_IsObject(j_parent))
|
|
@@ -1460,18 +1903,20 @@ static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1460
1903
|
}
|
|
1461
1904
|
}
|
|
1462
1905
|
|
|
1463
|
-
|
|
1906
|
+
struct js_entry_call
|
|
1464
1907
|
{
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
VALUE r_name = argv[0];
|
|
1469
|
-
|
|
1908
|
+
int argc;
|
|
1909
|
+
VALUE *argv;
|
|
1470
1910
|
VMData *data;
|
|
1471
|
-
|
|
1911
|
+
};
|
|
1472
1912
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1913
|
+
static VALUE call_global_function_body(VALUE p)
|
|
1914
|
+
{
|
|
1915
|
+
struct js_entry_call *call = (struct js_entry_call *)p;
|
|
1916
|
+
int argc = call->argc;
|
|
1917
|
+
VALUE *argv = call->argv;
|
|
1918
|
+
VMData *data = call->data;
|
|
1919
|
+
VALUE r_name = argv[0];
|
|
1475
1920
|
|
|
1476
1921
|
JSValue j_this = JS_UNDEFINED;
|
|
1477
1922
|
JSValue j_func;
|
|
@@ -1599,6 +2044,26 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
1599
2044
|
return to_rb_return_value(data->context, js_std_await(data->context, j_result));
|
|
1600
2045
|
}
|
|
1601
2046
|
|
|
2047
|
+
static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
2048
|
+
{
|
|
2049
|
+
if (argc < 1)
|
|
2050
|
+
rb_raise(rb_eArgError, "wrong number of arguments (given 0, expected 1+)");
|
|
2051
|
+
|
|
2052
|
+
VMData *data;
|
|
2053
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
2054
|
+
|
|
2055
|
+
check_disposed(data);
|
|
2056
|
+
check_oom_poisoned(data);
|
|
2057
|
+
|
|
2058
|
+
// evals_in_flight stays elevated for the whole call, not just the
|
|
2059
|
+
// JS_Eval / JS_Call moments: the body holds live JSValues across Ruby
|
|
2060
|
+
// calls that can yield the GVL (path-segment to_s, argument
|
|
2061
|
+
// conversion), and a concurrent dispose! landing in such a gap would
|
|
2062
|
+
// free the runtime out from under them.
|
|
2063
|
+
struct js_entry_call call = {argc, argv, data};
|
|
2064
|
+
return run_held_js_entry(data, call_global_function_body, (VALUE)&call);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1602
2067
|
static VALUE vm_m_set_module_loader(VALUE r_self, VALUE r_loader)
|
|
1603
2068
|
{
|
|
1604
2069
|
VMData *data;
|
|
@@ -1607,6 +2072,7 @@ static VALUE vm_m_set_module_loader(VALUE r_self, VALUE r_loader)
|
|
|
1607
2072
|
if (!NIL_P(r_loader) && !rb_obj_is_kind_of(r_loader, rb_cProc))
|
|
1608
2073
|
rb_raise(rb_eTypeError, "module_loader must be a Proc or nil");
|
|
1609
2074
|
|
|
2075
|
+
check_no_gvl_release_in_flight(data);
|
|
1610
2076
|
data->module_loader = r_loader;
|
|
1611
2077
|
// Stale entries from the previous loader's policy would survive the
|
|
1612
2078
|
// swap and silently shadow the new behavior.
|
|
@@ -1630,12 +2096,18 @@ static VALUE vm_m_on_unhandled_rejection(VALUE r_self)
|
|
|
1630
2096
|
VMData *data;
|
|
1631
2097
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1632
2098
|
|
|
2099
|
+
check_no_gvl_release_in_flight(data);
|
|
1633
2100
|
data->on_unhandled_rejection = rb_block_proc();
|
|
1634
2101
|
return Qnil;
|
|
1635
2102
|
}
|
|
1636
2103
|
|
|
1637
|
-
static VALUE
|
|
2104
|
+
static VALUE import_body(VALUE p)
|
|
1638
2105
|
{
|
|
2106
|
+
struct js_entry_call *call = (struct js_entry_call *)p;
|
|
2107
|
+
int argc = call->argc;
|
|
2108
|
+
VALUE *argv = call->argv;
|
|
2109
|
+
VMData *data = call->data;
|
|
2110
|
+
|
|
1639
2111
|
VALUE r_import_string, r_opts;
|
|
1640
2112
|
rb_scan_args(argc, argv, "10:", &r_import_string, &r_opts);
|
|
1641
2113
|
if (NIL_P(r_opts))
|
|
@@ -1652,11 +2124,6 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1652
2124
|
rb_raise(rb_eArgError, "pass either from: (inline source) or filename: (loader-resolved), not both");
|
|
1653
2125
|
VALUE r_custom_exposure = rb_hash_aref(r_opts, ID2SYM(rb_intern("code_to_expose")));
|
|
1654
2126
|
|
|
1655
|
-
VMData *data;
|
|
1656
|
-
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1657
|
-
|
|
1658
|
-
check_disposed(data);
|
|
1659
|
-
|
|
1660
2127
|
char *filename;
|
|
1661
2128
|
VALUE r_seeded_key = Qnil;
|
|
1662
2129
|
if (!NIL_P(r_filename))
|
|
@@ -1732,6 +2199,29 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
1732
2199
|
return Qtrue;
|
|
1733
2200
|
}
|
|
1734
2201
|
|
|
2202
|
+
static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
2203
|
+
{
|
|
2204
|
+
VMData *data;
|
|
2205
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
2206
|
+
|
|
2207
|
+
check_disposed(data);
|
|
2208
|
+
check_oom_poisoned(data);
|
|
2209
|
+
|
|
2210
|
+
// Module top-level code is user JS like any eval — budget it. Without
|
|
2211
|
+
// this, import ran under whatever handler the previous entry point
|
|
2212
|
+
// left behind: a lapsed armed one (spurious interrupt) or none at all
|
|
2213
|
+
// after a polyfill load's disarm (unbounded).
|
|
2214
|
+
arm_eval_timer(data);
|
|
2215
|
+
|
|
2216
|
+
// Like vm_m_callGlobalFunction: the body registers a module in the
|
|
2217
|
+
// context and then runs Ruby code (_build_import, StringValueCStr on
|
|
2218
|
+
// user values) that can yield the GVL before the bridge eval — keep
|
|
2219
|
+
// evals_in_flight elevated for the whole call so a concurrent dispose!
|
|
2220
|
+
// can't free the runtime in those gaps.
|
|
2221
|
+
struct js_entry_call call = {argc, argv, data};
|
|
2222
|
+
return run_held_js_entry(data, import_body, (VALUE)&call);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1735
2225
|
RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
|
|
1736
2226
|
{
|
|
1737
2227
|
rb_require("json");
|
|
@@ -1796,19 +2286,10 @@ static VALUE vm_m_runGC(VALUE r_self)
|
|
|
1796
2286
|
return Qnil;
|
|
1797
2287
|
}
|
|
1798
2288
|
|
|
1799
|
-
static VALUE
|
|
2289
|
+
static VALUE drain_jobs_body(VALUE p)
|
|
1800
2290
|
{
|
|
1801
|
-
VMData *data;
|
|
1802
|
-
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1803
|
-
|
|
1804
|
-
check_oom_poisoned(data);
|
|
1805
|
-
|
|
2291
|
+
VMData *data = (VMData *)p;
|
|
1806
2292
|
JSRuntime *runtime = JS_GetRuntime(data->context);
|
|
1807
|
-
if (!JS_IsJobPending(runtime))
|
|
1808
|
-
return INT2NUM(0);
|
|
1809
|
-
|
|
1810
|
-
arm_eval_timer(data);
|
|
1811
|
-
|
|
1812
2293
|
int executed = 0;
|
|
1813
2294
|
for (;;)
|
|
1814
2295
|
{
|
|
@@ -1822,6 +2303,22 @@ static VALUE vm_m_drainJobs(VALUE r_self)
|
|
|
1822
2303
|
return INT2NUM(executed);
|
|
1823
2304
|
}
|
|
1824
2305
|
|
|
2306
|
+
static VALUE vm_m_drainJobs(VALUE r_self)
|
|
2307
|
+
{
|
|
2308
|
+
VMData *data;
|
|
2309
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
2310
|
+
|
|
2311
|
+
check_disposed(data);
|
|
2312
|
+
check_oom_poisoned(data);
|
|
2313
|
+
|
|
2314
|
+
if (!JS_IsJobPending(JS_GetRuntime(data->context)))
|
|
2315
|
+
return INT2NUM(0);
|
|
2316
|
+
|
|
2317
|
+
arm_eval_timer(data);
|
|
2318
|
+
|
|
2319
|
+
return run_held_js_entry(data, drain_jobs_body, (VALUE)data);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
1825
2322
|
static VALUE vm_m_memoryPoisoned(VALUE r_self)
|
|
1826
2323
|
{
|
|
1827
2324
|
VMData *data;
|
|
@@ -1849,6 +2346,15 @@ static VALUE vm_m_dispose(VALUE r_self)
|
|
|
1849
2346
|
if (data->disposed)
|
|
1850
2347
|
return Qnil;
|
|
1851
2348
|
|
|
2349
|
+
// Freeing the runtime under live JS is a use-after-free. The overlap is
|
|
2350
|
+
// reachable both through the GVL release (pure-path evals) and through
|
|
2351
|
+
// GVL-yielding bridge callbacks (setTimeout's rb_thread_wait_for,
|
|
2352
|
+
// define_function procs, on_log listeners) — including the README's
|
|
2353
|
+
// `Thread.new { vm.dispose! }` pattern and a listener calling dispose!
|
|
2354
|
+
// mid-eval. Fail loudly instead of corrupting the heap.
|
|
2355
|
+
if (data->evals_in_flight > 0)
|
|
2356
|
+
rb_raise(rb_eThreadError, "cannot dispose a Quickjs::VM while it is evaluating");
|
|
2357
|
+
|
|
1852
2358
|
if (!JS_IsUndefined(data->j_file_proxy_creator))
|
|
1853
2359
|
{
|
|
1854
2360
|
JS_FreeValue(data->context, data->j_file_proxy_creator);
|
data/ext/quickjsrb/quickjsrb.h
CHANGED
|
@@ -75,8 +75,60 @@ typedef struct VMData
|
|
|
75
75
|
// enough pressure to collect the wrapper. Doubles as a double-free guard
|
|
76
76
|
// for the dfree handler.
|
|
77
77
|
bool disposed;
|
|
78
|
+
// Set while JS is executing with the GVL released so JS→Ruby bridges
|
|
79
|
+
// (currently js_quickjsrb_log) can re-acquire the GVL before touching
|
|
80
|
+
// Ruby APIs. Covers both vm_m_evalCode and vm_m_loadPolyfillBytecode;
|
|
81
|
+
// reset before to_rb_return_value or other Ruby-touching code runs.
|
|
82
|
+
// Saved/restored (not blindly cleared) so a region nested through an
|
|
83
|
+
// on_log listener doesn't clear the outer region's flag.
|
|
84
|
+
bool gvl_released_js;
|
|
85
|
+
// Number of JS executions currently in flight on this VM — eval_code
|
|
86
|
+
// (both the GVL-released and GVL-held paths), call, bytecode runs,
|
|
87
|
+
// polyfill bytecode loads, import, and job drains. vm_m_dispose refuses
|
|
88
|
+
// (ThreadError) while nonzero: freeing the runtime under live JS is a
|
|
89
|
+
// use-after-free, and both the GVL release and GVL-yielding bridge
|
|
90
|
+
// callbacks (setTimeout's rb_thread_wait_for, define_function procs,
|
|
91
|
+
// on_log listeners) make that overlap reachable — e.g. the README's
|
|
92
|
+
// `Thread.new { vm.dispose! }` pattern, or a listener calling dispose!
|
|
93
|
+
// mid-eval. Only mutated while holding the GVL, so plain int accesses
|
|
94
|
+
// are race-free.
|
|
95
|
+
int evals_in_flight;
|
|
96
|
+
// Number of GVL-release regions currently open on this VM (a subset of
|
|
97
|
+
// evals_in_flight). Bridge-registration APIs (define_function,
|
|
98
|
+
// module_loader=, on_unhandled_rejection) refuse (ThreadError) while
|
|
99
|
+
// nonzero: the running JS was allowed to release the GVL because
|
|
100
|
+
// can_eval_gvl_free held at eval start, and installing a bridge
|
|
101
|
+
// mid-flight — e.g. from an on_log listener, whose callback runs with
|
|
102
|
+
// the GVL re-acquired — would hand the still-released JS a path into
|
|
103
|
+
// Ruby APIs without the GVL. Only mutated while holding the GVL.
|
|
104
|
+
int gvl_release_regions;
|
|
105
|
+
// Latched by quickjsrb_new_ruby_bridge whenever a C function that calls
|
|
106
|
+
// into Ruby synchronously (rb_funcall & friends) WITHOUT honoring
|
|
107
|
+
// gvl_released_js is installed into this context. While true,
|
|
108
|
+
// can_eval_gvl_free fails and eval keeps the GVL held. Register every
|
|
109
|
+
// such function through that helper — a bridge registered with plain
|
|
110
|
+
// JS_NewCFunction would run against Ruby under a released GVL and
|
|
111
|
+
// silently corrupt the interpreter.
|
|
112
|
+
bool has_native_ruby_bridge;
|
|
78
113
|
} VMData;
|
|
79
114
|
|
|
115
|
+
// Drop-in replacement for JS_NewCFunction for C functions that call into
|
|
116
|
+
// Ruby synchronously without honoring gvl_released_js (crypto.*, File
|
|
117
|
+
// proxy helpers, setTimeout's job). Latching has_native_ruby_bridge here
|
|
118
|
+
// makes the "keep the GVL held" contract structural instead of relying on
|
|
119
|
+
// each feature-init site to remember a flag assignment. Requires
|
|
120
|
+
// JS_SetContextOpaque(ctx, data) to have run (vm_m_initialize does this
|
|
121
|
+
// before any feature setup). console.log intentionally does NOT go through
|
|
122
|
+
// this: js_quickjsrb_log re-acquires the GVL itself, and Proc-backed
|
|
123
|
+
// bridges (define_function / module_loader / on_unhandled_rejection) are
|
|
124
|
+
// excluded structurally by can_eval_gvl_free's own checks.
|
|
125
|
+
static inline JSValue quickjsrb_new_ruby_bridge(JSContext *ctx, JSCFunction *func, const char *name, int length)
|
|
126
|
+
{
|
|
127
|
+
VMData *data = JS_GetContextOpaque(ctx);
|
|
128
|
+
data->has_native_ruby_bridge = true;
|
|
129
|
+
return JS_NewCFunction(ctx, func, name, length);
|
|
130
|
+
}
|
|
131
|
+
|
|
80
132
|
static void vm_teardown_context(JSContext *ctx)
|
|
81
133
|
{
|
|
82
134
|
JSRuntime *runtime = JS_GetRuntime(ctx);
|
|
@@ -168,6 +220,10 @@ static VALUE vm_alloc(VALUE r_self)
|
|
|
168
220
|
data->j_file_proxy_creator = JS_UNDEFINED;
|
|
169
221
|
data->oom_poisoned = false;
|
|
170
222
|
data->disposed = false;
|
|
223
|
+
data->gvl_released_js = false;
|
|
224
|
+
data->evals_in_flight = 0;
|
|
225
|
+
data->gvl_release_regions = 0;
|
|
226
|
+
data->has_native_ruby_bridge = false;
|
|
171
227
|
|
|
172
228
|
EvalTime *eval_time = malloc(sizeof(EvalTime));
|
|
173
229
|
data->eval_time = eval_time;
|
|
@@ -63,9 +63,9 @@ void quickjsrb_init_crypto(JSContext *ctx, JSValue j_global)
|
|
|
63
63
|
{
|
|
64
64
|
JSValue j_crypto = JS_NewObject(ctx);
|
|
65
65
|
JS_SetPropertyStr(ctx, j_crypto, "getRandomValues",
|
|
66
|
-
|
|
66
|
+
quickjsrb_new_ruby_bridge(ctx, js_crypto_get_random_values, "getRandomValues", 1));
|
|
67
67
|
JS_SetPropertyStr(ctx, j_crypto, "randomUUID",
|
|
68
|
-
|
|
68
|
+
quickjsrb_new_ruby_bridge(ctx, js_crypto_random_uuid, "randomUUID", 0));
|
|
69
69
|
quickjsrb_init_crypto_subtle(ctx, j_crypto);
|
|
70
70
|
JS_SetPropertyStr(ctx, j_global, "crypto", j_crypto);
|
|
71
71
|
}
|
|
@@ -65,6 +65,7 @@ static VALUE js_usages_to_ruby_array(JSContext *ctx, JSValueConst j_usages)
|
|
|
65
65
|
static void js_reject_with_ruby_error(JSContext *ctx, JSValueConst *resolving_funcs)
|
|
66
66
|
{
|
|
67
67
|
VALUE r_error = rb_errinfo();
|
|
68
|
+
rb_set_errinfo(Qnil);
|
|
68
69
|
VALUE r_message = rb_funcall(r_error, rb_intern("message"), 0);
|
|
69
70
|
JSValue j_err = JS_NewError(ctx);
|
|
70
71
|
JS_SetPropertyStr(ctx, j_err, "message", JS_NewString(ctx, StringValueCStr(r_message)));
|
|
@@ -974,28 +975,28 @@ void quickjsrb_init_crypto_subtle(JSContext *ctx, JSValueConst j_crypto)
|
|
|
974
975
|
{
|
|
975
976
|
JSValue j_subtle = JS_NewObject(ctx);
|
|
976
977
|
JS_SetPropertyStr(ctx, j_subtle, "digest",
|
|
977
|
-
|
|
978
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_digest, "digest", 2));
|
|
978
979
|
JS_SetPropertyStr(ctx, j_subtle, "generateKey",
|
|
979
|
-
|
|
980
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_generate_key, "generateKey", 3));
|
|
980
981
|
JS_SetPropertyStr(ctx, j_subtle, "importKey",
|
|
981
|
-
|
|
982
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_import_key, "importKey", 5));
|
|
982
983
|
JS_SetPropertyStr(ctx, j_subtle, "exportKey",
|
|
983
|
-
|
|
984
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_export_key, "exportKey", 2));
|
|
984
985
|
JS_SetPropertyStr(ctx, j_subtle, "encrypt",
|
|
985
|
-
|
|
986
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_encrypt, "encrypt", 3));
|
|
986
987
|
JS_SetPropertyStr(ctx, j_subtle, "decrypt",
|
|
987
|
-
|
|
988
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_decrypt, "decrypt", 3));
|
|
988
989
|
JS_SetPropertyStr(ctx, j_subtle, "sign",
|
|
989
|
-
|
|
990
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_sign, "sign", 3));
|
|
990
991
|
JS_SetPropertyStr(ctx, j_subtle, "verify",
|
|
991
|
-
|
|
992
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_verify, "verify", 4));
|
|
992
993
|
JS_SetPropertyStr(ctx, j_subtle, "deriveBits",
|
|
993
|
-
|
|
994
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_derive_bits, "deriveBits", 3));
|
|
994
995
|
JS_SetPropertyStr(ctx, j_subtle, "deriveKey",
|
|
995
|
-
|
|
996
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_derive_key, "deriveKey", 5));
|
|
996
997
|
JS_SetPropertyStr(ctx, j_subtle, "wrapKey",
|
|
997
|
-
|
|
998
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_wrap_key, "wrapKey", 4));
|
|
998
999
|
JS_SetPropertyStr(ctx, j_subtle, "unwrapKey",
|
|
999
|
-
|
|
1000
|
+
quickjsrb_new_ruby_bridge(ctx, js_subtle_unwrap_key, "unwrapKey", 7));
|
|
1000
1001
|
JS_SetPropertyStr(ctx, j_crypto, "subtle", j_subtle);
|
|
1001
1002
|
}
|
|
@@ -203,13 +203,13 @@ void quickjsrb_init_file_proxy(VMData *data)
|
|
|
203
203
|
JSValue j_factory_fn = JS_Eval(data->context, factory_src, strlen(factory_src), "<file-proxy>", JS_EVAL_TYPE_GLOBAL);
|
|
204
204
|
|
|
205
205
|
JSValue j_helpers[7];
|
|
206
|
-
j_helpers[0] =
|
|
207
|
-
j_helpers[1] =
|
|
208
|
-
j_helpers[2] =
|
|
209
|
-
j_helpers[3] =
|
|
210
|
-
j_helpers[4] =
|
|
211
|
-
j_helpers[5] =
|
|
212
|
-
j_helpers[6] =
|
|
206
|
+
j_helpers[0] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_name, "__rb_file_name", 1);
|
|
207
|
+
j_helpers[1] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_size, "__rb_file_size", 1);
|
|
208
|
+
j_helpers[2] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_type, "__rb_file_type", 1);
|
|
209
|
+
j_helpers[3] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_last_modified, "__rb_file_last_modified", 1);
|
|
210
|
+
j_helpers[4] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_text, "__rb_file_text", 1);
|
|
211
|
+
j_helpers[5] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_array_buffer, "__rb_file_array_buffer", 1);
|
|
212
|
+
j_helpers[6] = quickjsrb_new_ruby_bridge(data->context, js_ruby_file_slice, "__rb_file_slice", 4);
|
|
213
213
|
|
|
214
214
|
data->j_file_proxy_creator = JS_Call(data->context, j_factory_fn, JS_UNDEFINED, 7, j_helpers);
|
|
215
215
|
|
data/lib/quickjs/polyfills.rb
CHANGED
|
@@ -8,13 +8,21 @@ module Quickjs
|
|
|
8
8
|
# `source:` accepts either a `String` (eager) or a `Proc` returning one
|
|
9
9
|
# (lazy). The lazy form lets a companion gem call `register_polyfill`
|
|
10
10
|
# at require time without paying the file-read cost unless a VM
|
|
11
|
-
# actually opts into the feature.
|
|
11
|
+
# actually opts into the feature. The Proc must not itself construct a
|
|
12
|
+
# VM with the same feature: compilation is locked per entry, so that
|
|
13
|
+
# recursion raises ThreadError instead of deadlocking silently.
|
|
14
|
+
#
|
|
15
|
+
# Re-registering a name replaces the entry wholesale, lock included: a
|
|
16
|
+
# VM construction already compiling under the old entry finishes
|
|
17
|
+
# against it (and loads the old bytecode) while the first construction
|
|
18
|
+
# under the new entry compiles the new source independently. Each
|
|
19
|
+
# registration compiles at most once; the two compiles can overlap.
|
|
12
20
|
def self.register_polyfill(name, source:, init: nil)
|
|
13
21
|
raise ::TypeError, "name must be a Symbol, got #{name.class}" unless name.is_a?(Symbol)
|
|
14
22
|
raise ::TypeError, "source: must be a String or Proc, got #{source.class}" unless source.is_a?(String) || source.is_a?(Proc)
|
|
15
23
|
raise ::TypeError, "init: must be a String or nil, got #{init.class}" unless init.nil? || init.is_a?(String)
|
|
16
24
|
|
|
17
|
-
@_polyfills[name] = {source: source, init: init&.freeze, bytecode: nil}
|
|
25
|
+
@_polyfills[name] = {source: source, init: init&.freeze, bytecode: nil, mutex: Mutex.new}
|
|
18
26
|
nil
|
|
19
27
|
end
|
|
20
28
|
|
|
@@ -30,15 +38,26 @@ module Quickjs
|
|
|
30
38
|
def self._apply_registered_polyfills(vm, features)
|
|
31
39
|
features.each do |feature|
|
|
32
40
|
next unless (entry = @_polyfills[feature])
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
# The per-entry mutex makes first-use compilation happen exactly once
|
|
42
|
+
# even when threads race to construct VMs with the same polyfill —
|
|
43
|
+
# `||=` alone isn't atomic (`_precompile_polyfill` yields the GVL
|
|
44
|
+
# inside `Quickjs.compile`), and a losing thread could otherwise
|
|
45
|
+
# double-compile or read entry[:source] after the winner cleared it.
|
|
46
|
+
# A compile failure leaves bytecode nil and source intact, so a later
|
|
47
|
+
# attempt can retry. The unlocked first read keeps the post-compile
|
|
48
|
+
# hot path (warmer pools constructing VMs concurrently) off the lock;
|
|
49
|
+
# a stale nil just falls into the synchronize.
|
|
50
|
+
bytecode = entry[:bytecode] || entry[:mutex].synchronize {
|
|
51
|
+
entry[:bytecode] ||= begin
|
|
52
|
+
compiled = _precompile_polyfill(entry, feature)
|
|
53
|
+
# The Proc / String source isn't needed again once the bytecode
|
|
54
|
+
# is cached — drop it so its captured scope can be GC'd.
|
|
55
|
+
entry[:source] = nil
|
|
56
|
+
entry[:init] = nil
|
|
57
|
+
compiled
|
|
58
|
+
end
|
|
59
|
+
}
|
|
60
|
+
vm.send(:_load_polyfill_bytecode, bytecode)
|
|
42
61
|
end
|
|
43
62
|
end
|
|
44
63
|
|
|
@@ -351,6 +351,8 @@ module Quickjs
|
|
|
351
351
|
cipher = OpenSSL::Cipher.new("aes-#{key.algorithm["length"]}-gcm")
|
|
352
352
|
cipher.encrypt
|
|
353
353
|
cipher.key = key.key_data
|
|
354
|
+
# WebCrypto allows any nonzero IV length for AES-GCM; OpenSSL defaults to 12 bytes
|
|
355
|
+
cipher.iv_len = iv.bytesize
|
|
354
356
|
cipher.iv = iv
|
|
355
357
|
cipher.auth_data = additional_data || ""
|
|
356
358
|
ciphertext = cipher.update(data) + cipher.final
|
|
@@ -365,12 +367,16 @@ module Quickjs
|
|
|
365
367
|
additional_data = params[:additional_data]
|
|
366
368
|
|
|
367
369
|
tag_bytes = tag_length / 8
|
|
370
|
+
if data.bytesize < tag_bytes
|
|
371
|
+
raise ArgumentError, "SubtleCrypto: AES-GCM data is shorter than the #{tag_bytes}-byte authentication tag"
|
|
372
|
+
end
|
|
368
373
|
ciphertext = data[0, data.bytesize - tag_bytes]
|
|
369
374
|
tag = data[-tag_bytes, tag_bytes]
|
|
370
375
|
|
|
371
376
|
decipher = OpenSSL::Cipher.new("aes-#{key.algorithm["length"]}-gcm")
|
|
372
377
|
decipher.decrypt
|
|
373
378
|
decipher.key = key.key_data
|
|
379
|
+
decipher.iv_len = iv.bytesize
|
|
374
380
|
decipher.iv = iv
|
|
375
381
|
decipher.auth_tag = tag
|
|
376
382
|
decipher.auth_data = additional_data || ""
|
data/lib/quickjs/version.rb
CHANGED
data/polyfills/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quickjs-rb-polyfills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0.rc1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "quickjs-rb-polyfills",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.21.0.rc1",
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"rolldown": "^1.0.0-rc.14"
|
|
12
12
|
}
|
data/polyfills/package.json
CHANGED