quickjs 0.21.0.rc1 → 0.22.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 +237 -7
- data/ext/quickjsrb/quickjsrb.c +3113 -556
- data/ext/quickjsrb/quickjsrb.h +401 -6
- data/ext/quickjsrb/quickjsrb_crypto_subtle.c +584 -76
- data/ext/quickjsrb/quickjsrb_file.c +253 -48
- data/lib/quickjs/crypto_key.rb +13 -0
- data/lib/quickjs/importable.rb +80 -0
- data/lib/quickjs/modules.rb +98 -0
- data/lib/quickjs/polyfills.rb +15 -1
- data/lib/quickjs/variables.rb +890 -0
- data/lib/quickjs/version.rb +1 -1
- data/lib/quickjs.rb +3 -0
- data/polyfills/package-lock.json +2 -2
- data/polyfills/package.json +1 -1
- data/sig/quickjs.rbs +20 -3
- metadata +4 -2
- data/ext/quickjsrb/quickjs/repl.c +0 -2057
data/ext/quickjsrb/quickjsrb.c
CHANGED
|
@@ -26,33 +26,119 @@ 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
28
|
static void check_disposed(VMData *data);
|
|
29
|
+
static void check_js_entry_owner(VMData *data);
|
|
29
30
|
static void run_gvl_release_region(VMData *data, void *(*job_run)(void *), void *job, JSValue *j_result, void *owned_buf0, void *owned_buf1);
|
|
31
|
+
// Defined below, next to the stack-bounds query it depends on; enter_js_entry
|
|
32
|
+
// above needs it.
|
|
33
|
+
static void rebase_stack_limit(VMData *data);
|
|
30
34
|
|
|
31
35
|
JSValue to_js_value(JSContext *ctx, VALUE r_value);
|
|
32
36
|
VALUE to_rb_value(JSContext *ctx, JSValue j_val);
|
|
33
|
-
|
|
37
|
+
VALUE to_rb_value_substituting(JSContext *ctx, JSValue j_val);
|
|
38
|
+
typedef struct ConvState ConvState;
|
|
39
|
+
static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, ConvState *conv);
|
|
40
|
+
static VALUE raise_js_exception(JSContext *ctx);
|
|
34
41
|
static VALUE vm_m_memoryUsage(VALUE r_self);
|
|
35
42
|
static VALUE vm_m_runGC(VALUE r_self);
|
|
43
|
+
static VALUE vm_m_poisoned(VALUE r_self);
|
|
36
44
|
static VALUE vm_m_memoryPoisoned(VALUE r_self);
|
|
37
45
|
static VALUE vm_m_dispose(VALUE r_self);
|
|
38
46
|
static VALUE vm_m_disposed(VALUE r_self);
|
|
39
47
|
static VALUE vm_m_drainJobs(VALUE r_self);
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
VALUE quickjsrb_secure_random = Qnil;
|
|
50
|
+
VALUE quickjsrb_handle_limit = Qnil;
|
|
51
|
+
|
|
52
|
+
void quickjsrb_init_handle_source(void)
|
|
42
53
|
{
|
|
43
|
-
|
|
54
|
+
quickjsrb_secure_random = rb_const_get(rb_cObject, rb_intern("SecureRandom"));
|
|
55
|
+
rb_gc_register_address(&quickjsrb_secure_random);
|
|
56
|
+
|
|
57
|
+
// Built without allocating, and so without a window where the global holds a
|
|
58
|
+
// value nothing roots yet: 2 ** 48 is a Fixnum here but a Bignum where a long
|
|
59
|
+
// is 32 bits, and the second call could have collected the first.
|
|
60
|
+
quickjsrb_handle_limit = LL2NUM(((int64_t)1 << QUICKJSRB_HANDLE_BITS) - 1);
|
|
61
|
+
rb_gc_register_address(&quickjsrb_handle_limit);
|
|
62
|
+
}
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
JSValue j_error_from_ruby_error(JSContext *ctx, VALUE r_error)
|
|
65
|
+
{
|
|
66
|
+
// Everything that can raise happens before anything is registered. Asking
|
|
67
|
+
// the exception for its message runs Ruby the host wrote, and a String with
|
|
68
|
+
// a null byte makes StringValueCStr raise; either longjmps out of here, and
|
|
69
|
+
// an entry made first would be parked in both tables for the life of the VM
|
|
70
|
+
// with nothing left pointing at it.
|
|
71
|
+
VALUE r_exception_message = rb_funcall(r_error, rb_intern("message"), 0);
|
|
72
|
+
const char *errorMessage = StringValueCStr(r_exception_message);
|
|
48
73
|
|
|
49
|
-
//
|
|
74
|
+
// The draw is here with them, not below: it calls SecureRandom and writes two
|
|
75
|
+
// hashes, any of which can raise, and a raise after the allocations would
|
|
76
|
+
// longjmp out of a JSCFunction leaving them, and the promise capability of
|
|
77
|
+
// whichever caller is mid-flight, unreleased.
|
|
50
78
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
51
|
-
|
|
79
|
+
// Taken before the draw, so the rollback below does not dispatch from inside
|
|
80
|
+
// a JSCFunction on its way out.
|
|
81
|
+
VALUE r_error_object_id = rb_obj_id(r_error);
|
|
82
|
+
bool registered_here = false;
|
|
83
|
+
VALUE r_object_id = alive_objects_register(data, r_error, ®istered_here);
|
|
84
|
+
if (NIL_P(r_object_id))
|
|
85
|
+
// No handle means no way to hand this exception back out, so the guest gets
|
|
86
|
+
// an error of its own rather than one that looks bridged and is not.
|
|
87
|
+
return JS_ThrowInternalError(ctx, "quickjs: could not publish a handle for a host exception");
|
|
52
88
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
89
|
+
JSValue j_error = JS_NewError(ctx); // may wanna have custom error class to determine in JS' end
|
|
90
|
+
JSValue j_message = JS_NewString(ctx, errorMessage);
|
|
91
|
+
// Both are allocations, and either answers JS_EXCEPTION on a heap that has
|
|
92
|
+
// run out. Storing the sentinel would put it on the error as an ordinary
|
|
93
|
+
// value and mask the throw that produced it, so neither is published and the
|
|
94
|
+
// caller is told instead.
|
|
95
|
+
if (JS_IsException(j_error) || JS_IsException(j_message))
|
|
96
|
+
{
|
|
97
|
+
if (registered_here)
|
|
98
|
+
alive_objects_unregister(data, r_object_id, r_error_object_id);
|
|
99
|
+
JS_FreeValue(ctx, j_error);
|
|
100
|
+
JS_FreeValue(ctx, j_message);
|
|
101
|
+
return JS_EXCEPTION;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Defined, not set: JS_SetPropertyStr walks the prototype chain, so an
|
|
105
|
+
// accessor a guest installs on Error.prototype absorbs the write and no own
|
|
106
|
+
// property is ever created, while the reader insists on an own data one.
|
|
107
|
+
// Every host exception would then be both unreportable and permanently
|
|
108
|
+
// parked, on one line of guest setup. Non-enumerable for the same reason the
|
|
109
|
+
// File and CryptoKey writers are: the handle is not part of the error.
|
|
110
|
+
//
|
|
111
|
+
// The message is defined for the same reason, and is non-enumerable with it,
|
|
112
|
+
// which is a change: assignment made it enumerable, so a bridged error was
|
|
113
|
+
// the one Error in the runtime whose message showed up in JSON.stringify and
|
|
114
|
+
// Object.keys. Errors the guest makes itself do not, because that is what the
|
|
115
|
+
// Error constructor does, and a bridged one is not special enough to differ.
|
|
116
|
+
if (JS_DefinePropertyValueStr(ctx, j_error, "rb_object_id",
|
|
117
|
+
JS_NewInt64(ctx, NUM2LL(r_object_id)),
|
|
118
|
+
JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0)
|
|
119
|
+
{
|
|
120
|
+
// Only the row this call made. The registrar reuses one when the same
|
|
121
|
+
// object is bridged twice, and tearing that down would unanchor the error
|
|
122
|
+
// an earlier crossing is still holding.
|
|
123
|
+
if (registered_here)
|
|
124
|
+
{
|
|
125
|
+
alive_objects_unregister(data, r_object_id, r_error_object_id);
|
|
126
|
+
}
|
|
127
|
+
JS_FreeValue(ctx, j_message);
|
|
128
|
+
JS_FreeValue(ctx, j_error);
|
|
129
|
+
return JS_EXCEPTION;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (JS_DefinePropertyValueStr(ctx, j_error, "message", j_message,
|
|
133
|
+
JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0)
|
|
134
|
+
{
|
|
135
|
+
if (registered_here)
|
|
136
|
+
{
|
|
137
|
+
alive_objects_unregister(data, r_object_id, r_error_object_id);
|
|
138
|
+
}
|
|
139
|
+
JS_FreeValue(ctx, j_error);
|
|
140
|
+
return JS_EXCEPTION;
|
|
141
|
+
}
|
|
56
142
|
|
|
57
143
|
return j_error;
|
|
58
144
|
}
|
|
@@ -61,6 +147,7 @@ typedef struct
|
|
|
61
147
|
{
|
|
62
148
|
JSContext *ctx;
|
|
63
149
|
JSValue j_obj;
|
|
150
|
+
bool failed;
|
|
64
151
|
} RbHashToJsArg;
|
|
65
152
|
|
|
66
153
|
static int rb_hash_entry_to_js(VALUE r_key, VALUE r_val, VALUE extra)
|
|
@@ -80,7 +167,30 @@ static int rb_hash_entry_to_js(VALUE r_key, VALUE r_val, VALUE extra)
|
|
|
80
167
|
VALUE r_key_str = rb_funcall(r_key, rb_intern("to_s"), 0);
|
|
81
168
|
key_cstr = StringValueCStr(r_key_str);
|
|
82
169
|
}
|
|
83
|
-
|
|
170
|
+
JSValue j_val = to_js_value(arg->ctx, r_val);
|
|
171
|
+
// A member that could not be built is not stored as the sentinel: doing so
|
|
172
|
+
// makes any later read of that slot behave as though an exception were
|
|
173
|
+
// pending. The walk stops and the caller is told.
|
|
174
|
+
if (JS_IsException(j_val))
|
|
175
|
+
{
|
|
176
|
+
arg->failed = true;
|
|
177
|
+
return ST_STOP;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Defined, not assigned, on an object we just built and are about to hand
|
|
181
|
+
// over: JS_SetPropertyStr walks the prototype chain, so a setter the guest
|
|
182
|
+
// installed on Object.prototype under this key receives the host's value and
|
|
183
|
+
// the object goes back with no own property of that name at all. It also
|
|
184
|
+
// keeps a "__proto__" key a key, rather than letting it reparent the object.
|
|
185
|
+
if (JS_DefinePropertyValueStr(arg->ctx, arg->j_obj, key_cstr, j_val, JS_PROP_C_W_E) < 0)
|
|
186
|
+
{
|
|
187
|
+
// Reported rather than shrugged off: the object would otherwise go back
|
|
188
|
+
// looking complete, missing that one key, with the throw the define set
|
|
189
|
+
// left for whatever asks next. A key the guest can then answer for with a
|
|
190
|
+
// prototype accessor is exactly what defining it was meant to prevent.
|
|
191
|
+
arg->failed = true;
|
|
192
|
+
return ST_STOP;
|
|
193
|
+
}
|
|
84
194
|
return ST_CONTINUE;
|
|
85
195
|
}
|
|
86
196
|
|
|
@@ -96,15 +206,13 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
|
|
|
96
206
|
return JS_NewFloat64(ctx, NUM2DBL(r_value));
|
|
97
207
|
case T_BIGNUM:
|
|
98
208
|
{
|
|
209
|
+
// strtod on the decimal we just produced, rather than calling globalThis's
|
|
210
|
+
// Number: that name is the guest's to delete or replace with a shim that
|
|
211
|
+
// throws, and a Bignum crossing over is not something it gets to answer
|
|
212
|
+
// for. The value is the same one Number(str) would have produced, since
|
|
213
|
+
// this is the conversion Number(str) performs.
|
|
99
214
|
VALUE r_str = rb_funcall(r_value, rb_intern("to_s"), 0);
|
|
100
|
-
|
|
101
|
-
JSValue j_global = JS_GetGlobalObject(ctx);
|
|
102
|
-
JSValue j_numberClass = JS_GetPropertyStr(ctx, j_global, "Number");
|
|
103
|
-
JSValue j_num = JS_Call(ctx, j_numberClass, JS_UNDEFINED, 1, (JSValueConst *)&j_str);
|
|
104
|
-
JS_FreeValue(ctx, j_str);
|
|
105
|
-
JS_FreeValue(ctx, j_numberClass);
|
|
106
|
-
JS_FreeValue(ctx, j_global);
|
|
107
|
-
return j_num;
|
|
215
|
+
return JS_NewFloat64(ctx, rb_str_to_dbl(r_str, FALSE));
|
|
108
216
|
}
|
|
109
217
|
case T_STRING:
|
|
110
218
|
return JS_NewStringLen(ctx, RSTRING_PTR(r_value), RSTRING_LEN(r_value));
|
|
@@ -132,15 +240,36 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
|
|
|
132
240
|
JSValue j_arr = JS_NewArray(ctx);
|
|
133
241
|
for (int i = 0; i < len; i++)
|
|
134
242
|
{
|
|
135
|
-
|
|
243
|
+
JSValue j_element = to_js_value(ctx, RARRAY_AREF(r_value, i));
|
|
244
|
+
// See rb_hash_entry_to_js: an element that could not be built is not
|
|
245
|
+
// stored as the sentinel.
|
|
246
|
+
if (JS_IsException(j_element))
|
|
247
|
+
{
|
|
248
|
+
JS_FreeValue(ctx, j_arr);
|
|
249
|
+
return JS_EXCEPTION;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Defined for the same reason, which reaches elements too: any index
|
|
253
|
+
// property on Array.prototype costs the fast array path, and the write
|
|
254
|
+
// then walks the chain into the guest's setter.
|
|
255
|
+
if (JS_DefinePropertyValueUint32(ctx, j_arr, (uint32_t)i, j_element, JS_PROP_C_W_E) < 0)
|
|
256
|
+
{
|
|
257
|
+
JS_FreeValue(ctx, j_arr);
|
|
258
|
+
return JS_EXCEPTION;
|
|
259
|
+
}
|
|
136
260
|
}
|
|
137
261
|
return j_arr;
|
|
138
262
|
}
|
|
139
263
|
case T_HASH:
|
|
140
264
|
{
|
|
141
265
|
JSValue j_obj = JS_NewObject(ctx);
|
|
142
|
-
RbHashToJsArg arg = {ctx, j_obj};
|
|
266
|
+
RbHashToJsArg arg = {ctx, j_obj, false};
|
|
143
267
|
rb_hash_foreach(r_value, rb_hash_entry_to_js, (VALUE)&arg);
|
|
268
|
+
if (arg.failed)
|
|
269
|
+
{
|
|
270
|
+
JS_FreeValue(ctx, j_obj);
|
|
271
|
+
return JS_EXCEPTION;
|
|
272
|
+
}
|
|
144
273
|
return j_obj;
|
|
145
274
|
}
|
|
146
275
|
default:
|
|
@@ -153,7 +282,7 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
|
|
|
153
282
|
}
|
|
154
283
|
if (rb_obj_is_kind_of(r_value, rb_eException))
|
|
155
284
|
{
|
|
156
|
-
return j_error_from_ruby_error(ctx, r_value);
|
|
285
|
+
return j_error_from_ruby_error(ctx, r_value); // JS_EXCEPTION propagates to the caller
|
|
157
286
|
}
|
|
158
287
|
VALUE r_inspect_str = rb_funcall(r_value, rb_intern("inspect"), 0);
|
|
159
288
|
char *str = StringValueCStr(r_inspect_str);
|
|
@@ -163,20 +292,180 @@ JSValue to_js_value(JSContext *ctx, VALUE r_value)
|
|
|
163
292
|
}
|
|
164
293
|
}
|
|
165
294
|
|
|
295
|
+
static VALUE find_ruby_error_at(JSContext *ctx, JSValue j_error, int64_t *peek_handle);
|
|
296
|
+
|
|
297
|
+
static bool js_is_proxy(JSContext *ctx, JSValue j_val)
|
|
298
|
+
{
|
|
299
|
+
VMData *data = JS_GetContextOpaque(ctx);
|
|
300
|
+
return JS_IsObject(j_val) && data->proxy_class_id != 0 && JS_GetClassID(j_val) == data->proxy_class_id;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// With peek_handle, the exception is left in alive_objects and its handle
|
|
304
|
+
// written there, for a caller that may take it later.
|
|
305
|
+
static VALUE find_ruby_error_peeking(JSContext *ctx, JSValue j_error, int64_t *peek_handle)
|
|
306
|
+
{
|
|
307
|
+
// Whether anything was already pending before we read anything. Without it,
|
|
308
|
+
// a throw some earlier unchecked read left set looks exactly like one our own
|
|
309
|
+
// read just caused, and an unrelated conversion is handed the host exception
|
|
310
|
+
// it carried.
|
|
311
|
+
// Held, not just noted: a trap that throws during the read replaces what was
|
|
312
|
+
// pending, and JS_Throw frees the old one, so asking "was something pending"
|
|
313
|
+
// afterwards cannot tell a throw we caused from one that was already there.
|
|
314
|
+
// Comparing the values can.
|
|
315
|
+
//
|
|
316
|
+
// Whether it was uncatchable is noted with it: JS_Throw clears that flag and
|
|
317
|
+
// JS_GetException leaves it alone, so putting a lapsed budget's interrupt
|
|
318
|
+
// back without it would hand the guest something it can catch and return
|
|
319
|
+
// from, which is what j_rethrow_the_guests_own guards against by hand.
|
|
320
|
+
JSValue j_was_pending = JS_HasException(ctx) ? JS_GetException(ctx) : JS_UNINITIALIZED;
|
|
321
|
+
bool was_pending = !JS_IsUninitialized(j_was_pending);
|
|
322
|
+
bool was_uncatchable = was_pending && eval_budget_lapsed_now(ctx);
|
|
323
|
+
if (was_pending)
|
|
324
|
+
{
|
|
325
|
+
JS_Throw(ctx, JS_DupValue(ctx, j_was_pending));
|
|
326
|
+
if (was_uncatchable)
|
|
327
|
+
JS_SetUncatchableException(ctx, TRUE);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
VALUE r_error = find_ruby_error_at(ctx, j_error, peek_handle);
|
|
331
|
+
if (!NIL_P(r_error) || !JS_HasException(ctx))
|
|
332
|
+
{
|
|
333
|
+
JS_FreeValue(ctx, j_was_pending);
|
|
334
|
+
return r_error;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
JSValue j_trap_threw = JS_GetException(ctx);
|
|
338
|
+
if (was_pending && JS_VALUE_GET_PTR(j_trap_threw) == JS_VALUE_GET_PTR(j_was_pending))
|
|
339
|
+
{
|
|
340
|
+
// The same throw is still sitting there, so it is not ours to take.
|
|
341
|
+
JS_Throw(ctx, j_trap_threw);
|
|
342
|
+
if (was_uncatchable)
|
|
343
|
+
JS_SetUncatchableException(ctx, TRUE);
|
|
344
|
+
JS_FreeValue(ctx, j_was_pending);
|
|
345
|
+
return r_error;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// The read was answered by a Proxy's descriptor trap, and the trap threw.
|
|
349
|
+
// That throw is taken rather than left for an unrelated evaluation, and asked
|
|
350
|
+
// once whether it carries a host exception, because a bridge reached from the
|
|
351
|
+
// trap parks one and only this takes it back out.
|
|
352
|
+
//
|
|
353
|
+
// Once, and not if what it threw is itself a Proxy: asking that would run
|
|
354
|
+
// another trap, which is a chain the guest chooses the length of. Refusing at
|
|
355
|
+
// this level costs a bridged error wrapped in a Proxy thrown by a trap, and
|
|
356
|
+
// keeps one wrapped in a Proxy thrown directly, which is the shape that
|
|
357
|
+
// reaches here in practice.
|
|
358
|
+
if (!js_is_proxy(ctx, j_trap_threw))
|
|
359
|
+
r_error = find_ruby_error_at(ctx, j_trap_threw, peek_handle);
|
|
360
|
+
JS_FreeValue(ctx, j_trap_threw);
|
|
361
|
+
|
|
362
|
+
// Put back what was pending before any of this, which the trap's throw
|
|
363
|
+
// displaced. Taking the trap's throw is this function's business; losing an
|
|
364
|
+
// unrelated one that was already travelling is not, and the guard at the top
|
|
365
|
+
// exists to say so.
|
|
366
|
+
if (was_pending)
|
|
367
|
+
{
|
|
368
|
+
JS_Throw(ctx, j_was_pending);
|
|
369
|
+
if (was_uncatchable)
|
|
370
|
+
JS_SetUncatchableException(ctx, TRUE);
|
|
371
|
+
}
|
|
372
|
+
return r_error;
|
|
373
|
+
}
|
|
374
|
+
|
|
166
375
|
VALUE find_ruby_error(JSContext *ctx, JSValue j_error)
|
|
167
376
|
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
377
|
+
return find_ruby_error_peeking(ctx, j_error, NULL);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Takes a peeked exception out of alive_objects, unless something took it since.
|
|
381
|
+
static void release_peeked_ruby_error(VMData *data, int64_t handle, VALUE r_error)
|
|
382
|
+
{
|
|
383
|
+
VALUE r_key = LL2NUM(handle);
|
|
384
|
+
if (rb_hash_lookup2(data->alive_objects, r_key, Qundef) != r_error)
|
|
385
|
+
return;
|
|
386
|
+
VALUE r_error_object_id = rb_obj_id(r_error);
|
|
387
|
+
rb_hash_delete(data->alive_objects, r_key);
|
|
388
|
+
rb_hash_delete(data->alive_handles, r_error_object_id);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static VALUE find_ruby_error_at(JSContext *ctx, JSValue j_error, int64_t *peek_handle)
|
|
392
|
+
{
|
|
393
|
+
// Most callers know they hold an Error before they ask, but the one that
|
|
394
|
+
// inspects a failed conversion's throw cannot: `throw null` and `throw 1` are
|
|
395
|
+
// both legal. Reading a property off a primitive throws a TypeError that
|
|
396
|
+
// nothing here consumes, which would leave the runtime carrying a pending
|
|
397
|
+
// exception into the next evaluation — the very thing to_r_json goes out of
|
|
398
|
+
// its way to clear. A non-object was never a bridged Ruby error anyway.
|
|
399
|
+
if (!JS_IsObject(j_error))
|
|
400
|
+
return Qnil;
|
|
401
|
+
|
|
402
|
+
// Read as an own data property, never through a getter: all three writers
|
|
403
|
+
// define it that way, so nothing legitimate is missed, and a getter on a
|
|
404
|
+
// thrown object would otherwise reach a bridge from inside the lookup that
|
|
405
|
+
// exists to take a bridged exception back out.
|
|
406
|
+
//
|
|
407
|
+
// Not quite "runs no guest code", which is what this said before: a Proxy's
|
|
408
|
+
// getOwnPropertyDescriptor trap answers this read too, and a trap that
|
|
409
|
+
// throws makes JS_GetOwnProperty answer -1 with its throw left pending.
|
|
410
|
+
// That is a different answer from "no such property" and is kept apart from
|
|
411
|
+
// it, because folding the two left the trap's throw set and its parked
|
|
412
|
+
// exception unreachable.
|
|
413
|
+
JSAtom handle_atom = JS_NewAtom(ctx, "rb_object_id");
|
|
414
|
+
JSPropertyDescriptor desc;
|
|
415
|
+
int found = JS_GetOwnProperty(ctx, &desc, j_error, handle_atom);
|
|
416
|
+
JS_FreeAtom(ctx, handle_atom);
|
|
417
|
+
// A trap that threw leaves its throw pending, which find_ruby_error takes.
|
|
418
|
+
if (found <= 0)
|
|
419
|
+
return Qnil;
|
|
420
|
+
if ((desc.flags & JS_PROP_TMASK) != JS_PROP_NORMAL)
|
|
171
421
|
{
|
|
172
|
-
|
|
422
|
+
JS_FreeValue(ctx, desc.value);
|
|
423
|
+
JS_FreeValue(ctx, desc.getter);
|
|
424
|
+
JS_FreeValue(ctx, desc.setter);
|
|
425
|
+
return Qnil;
|
|
426
|
+
}
|
|
427
|
+
JS_FreeValue(ctx, desc.getter);
|
|
428
|
+
JS_FreeValue(ctx, desc.setter);
|
|
429
|
+
JSValue j_errorOriginalRubyObjectId = desc.value;
|
|
430
|
+
int64_t errorOriginalRubyObjectId = 0;
|
|
431
|
+
// FLOAT64 as well as INT: the handle is drawn from a range wider than a
|
|
432
|
+
// tagged int, so QuickJS carries most of them as doubles.
|
|
433
|
+
if (JS_VALUE_GET_NORM_TAG(j_errorOriginalRubyObjectId) == JS_TAG_INT || JS_VALUE_GET_NORM_TAG(j_errorOriginalRubyObjectId) == JS_TAG_FLOAT64)
|
|
434
|
+
{
|
|
435
|
+
JS_ToInt64(ctx, &errorOriginalRubyObjectId, j_errorOriginalRubyObjectId);
|
|
173
436
|
JS_FreeValue(ctx, j_errorOriginalRubyObjectId);
|
|
174
437
|
if (errorOriginalRubyObjectId > 0)
|
|
175
438
|
{
|
|
176
439
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
177
|
-
VALUE r_key =
|
|
440
|
+
VALUE r_key = LL2NUM(errorOriginalRubyObjectId);
|
|
178
441
|
VALUE r_error = rb_hash_aref(data->alive_objects, r_key);
|
|
442
|
+
// alive_objects anchors three unrelated things: a bridged exception,
|
|
443
|
+
// waiting to be thrown back, and the Ruby objects behind a File or a
|
|
444
|
+
// CryptoKey proxy, which stay reachable for as long as the guest holds
|
|
445
|
+
// the proxy. Only the first is this function's to take. The id is an
|
|
446
|
+
// ordinary property the guest can write, so without the check a script
|
|
447
|
+
// that copies a live proxy's id onto anything throwable severs that
|
|
448
|
+
// proxy: the entry is deleted, and every property of the File it stood
|
|
449
|
+
// for reads back as undefined while the object still passes
|
|
450
|
+
// `instanceof File`. Leaving a foreign entry anchored is the same
|
|
451
|
+
// answer as never having matched.
|
|
452
|
+
if (!rb_obj_is_kind_of(r_error, rb_eException))
|
|
453
|
+
return Qnil;
|
|
454
|
+
if (peek_handle != NULL)
|
|
455
|
+
{
|
|
456
|
+
*peek_handle = errorOriginalRubyObjectId;
|
|
457
|
+
return r_error;
|
|
458
|
+
}
|
|
459
|
+
// Both rows, and the reverse one's key taken before either delete:
|
|
460
|
+
// rb_obj_id rather than the object_id method, which a subclass may
|
|
461
|
+
// override and which may allocate, so asking after the first delete
|
|
462
|
+
// could unanchor the exception and then lose it on the way to the
|
|
463
|
+
// second. The reverse row is only there so an object bridged twice keeps
|
|
464
|
+
// one handle, and once the entry is gone there is nothing for it to
|
|
465
|
+
// point at.
|
|
466
|
+
VALUE r_error_object_id = rb_obj_id(r_error);
|
|
179
467
|
rb_hash_delete(data->alive_objects, r_key);
|
|
468
|
+
rb_hash_delete(data->alive_handles, r_error_object_id);
|
|
180
469
|
return r_error;
|
|
181
470
|
}
|
|
182
471
|
}
|
|
@@ -187,6 +476,38 @@ VALUE find_ruby_error(JSContext *ctx, JSValue j_error)
|
|
|
187
476
|
return Qnil;
|
|
188
477
|
}
|
|
189
478
|
|
|
479
|
+
// Takes a throw nobody is going to report, without losing what it carries. A
|
|
480
|
+
// read that runs the guest's own code can reach a Ruby bridge, and the bridge
|
|
481
|
+
// parks the host's exception in alive_objects on the way out; find_ruby_error
|
|
482
|
+
// is the only thing that takes one back out, so a reader that merely frees the
|
|
483
|
+
// JS_EXCEPTION pins one per call, at a rate the guest picks. The exception
|
|
484
|
+
// itself is still lost, which is the caller's business to say.
|
|
485
|
+
void quickjsrb_drain_pending(JSContext *ctx)
|
|
486
|
+
{
|
|
487
|
+
// One pass here. The reader it calls can run a Proxy's descriptor trap, and
|
|
488
|
+
// takes what that throws itself, so a drain can run guest code and this is
|
|
489
|
+
// not the pure cleanup it looks like.
|
|
490
|
+
//
|
|
491
|
+
// A budget that lapsed inside one of those getters goes with the rest, and
|
|
492
|
+
// that is not right: the sibling drain in js_hold_cstring_or_null latches it.
|
|
493
|
+
// Latching here does not help on its own, though. Re-throwing the interrupt
|
|
494
|
+
// leaves it pending for a caller that carries on regardless, which is how the
|
|
495
|
+
// guest's own error is lost too. Both want the refusal path in #130, and
|
|
496
|
+
// both are the same on main.
|
|
497
|
+
//
|
|
498
|
+
// Unparking here and not in j_rethrow_the_guests_own is deliberate, and the
|
|
499
|
+
// difference is what happens to the throw. That one hands it back to the
|
|
500
|
+
// guest, so it can still travel out and be reported as the host exception it
|
|
501
|
+
// was, and taking the entry would break that. This one drops it, so nothing
|
|
502
|
+
// will carry it out and an anchor left behind is only a leak. What it costs
|
|
503
|
+
// is a guest that kept its own reference: rethrowing that later reports the
|
|
504
|
+
// message but a generic class, since the handle it carries no longer
|
|
505
|
+
// resolves.
|
|
506
|
+
JSValue j_pending = JS_GetException(ctx);
|
|
507
|
+
find_ruby_error(ctx, j_pending);
|
|
508
|
+
JS_FreeValue(ctx, j_pending);
|
|
509
|
+
}
|
|
510
|
+
|
|
190
511
|
VALUE r_try_json_parse(VALUE r_str)
|
|
191
512
|
{
|
|
192
513
|
return rb_funcall(rb_const_get(rb_cClass, rb_intern("JSON")), rb_intern("parse"), 1, r_str);
|
|
@@ -203,7 +524,14 @@ static VALUE r_backtrace_from_js_stack(const char *stack)
|
|
|
203
524
|
if (stack == NULL || stack[0] == '\0')
|
|
204
525
|
return Qnil;
|
|
205
526
|
|
|
206
|
-
|
|
527
|
+
// Through rb_funcall and not rb_str_split: the latter enters String#split
|
|
528
|
+
// directly, so it honours whatever block the current C frame was called
|
|
529
|
+
// with. Any entry point has one if the caller passed one — eval_code with a
|
|
530
|
+
// stray block, define_function always — and there split yielded every line
|
|
531
|
+
// of the JS stack to that block and answered the String instead of an
|
|
532
|
+
// Array, which rb_ary_entry then dereferenced as one. rb_funcall never
|
|
533
|
+
// forwards a block.
|
|
534
|
+
VALUE r_lines = rb_funcall(rb_str_new_cstr(stack), rb_intern("split"), 1, rb_str_new_cstr("\n"));
|
|
207
535
|
VALUE r_filtered = rb_ary_new();
|
|
208
536
|
for (long i = 0; i < RARRAY_LEN(r_lines); i++)
|
|
209
537
|
{
|
|
@@ -255,49 +583,653 @@ static int js_is_plain_object(JSContext *ctx, JSValue j_val)
|
|
|
255
583
|
return result;
|
|
256
584
|
}
|
|
257
585
|
|
|
258
|
-
|
|
586
|
+
#define CONV_PINNED_INLINE 8
|
|
587
|
+
#define CONV_FRAMES_INLINE 8
|
|
588
|
+
|
|
589
|
+
// One frame per object the walk has entered. The walk borrows JS references —
|
|
590
|
+
// the property table, and the element, property or toJSON result currently
|
|
591
|
+
// being converted — and holds them across a recursive call. A Ruby exception
|
|
592
|
+
// raised anywhere below longjmps straight past the frees, so the frame holds
|
|
593
|
+
// those references on the walk's behalf and conv_release hands them back.
|
|
594
|
+
//
|
|
595
|
+
// The references live in the frame rather than being pointed at, because the C
|
|
596
|
+
// frames that borrowed them are already unwound by the time the ensure runs.
|
|
597
|
+
typedef struct
|
|
259
598
|
{
|
|
260
|
-
|
|
599
|
+
JSPropertyEnum *ptab; // property table this frame owns, NULL when it has none
|
|
600
|
+
uint32_t plen;
|
|
601
|
+
JSValue j_borrowed; // value being converted, JS_UNDEFINED when between two
|
|
602
|
+
const char *key; // property name being converted, NULL when between two
|
|
603
|
+
} ConvFrame;
|
|
604
|
+
|
|
605
|
+
// State threaded through the conversion of one JS object graph.
|
|
606
|
+
//
|
|
607
|
+
// `r_seen` maps an object's address either to CONV_IN_PROGRESS, meaning the
|
|
608
|
+
// object is an ancestor of the one being converted, or to the Ruby value it
|
|
609
|
+
// finished converting to. The first case is a cycle and becomes nil; the second
|
|
610
|
+
// is a shared subgraph, which converts once and stays shared on the Ruby side.
|
|
611
|
+
// Only the containers this conversion builds are kept — see the toJSON branch.
|
|
612
|
+
//
|
|
613
|
+
// Every tracked object is pinned with JS_DupValue until the whole conversion
|
|
614
|
+
// finishes. Property getters and toJSON run guest JS, which can drop the last
|
|
615
|
+
// reference to an object and let a new one be allocated at the same address;
|
|
616
|
+
// without the pin the map would answer for an object that no longer exists.
|
|
617
|
+
struct ConvState
|
|
618
|
+
{
|
|
619
|
+
JSContext *ctx;
|
|
620
|
+
JSValue j_root;
|
|
621
|
+
VALUE r_seen;
|
|
622
|
+
JSValue *pinned;
|
|
623
|
+
long pinned_count;
|
|
624
|
+
long pinned_capacity;
|
|
625
|
+
JSValue pinned_inline[CONV_PINNED_INLINE];
|
|
626
|
+
ConvFrame *frames;
|
|
627
|
+
long frame_count;
|
|
628
|
+
long frame_capacity;
|
|
629
|
+
ConvFrame frames_inline[CONV_FRAMES_INLINE];
|
|
630
|
+
// Set when the caller cannot be handed an exception: a console.log row is
|
|
631
|
+
// built inside a JSCFunction, so raising from there would unwind through
|
|
632
|
+
// QuickJS, and it would cost the guest the rest of the statement and the
|
|
633
|
+
// convertible arguments beside the offending one.
|
|
634
|
+
int substitute_unresolvable;
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// Frames move when the stack grows, so a walk holds its depth and re-derives
|
|
638
|
+
// the pointer rather than keeping one across a recursive call.
|
|
639
|
+
#define CONV_FRAME(conv, depth) (&(conv)->frames[depth])
|
|
640
|
+
|
|
641
|
+
// The map is private to one conversion, so using it as its own marker cannot
|
|
642
|
+
// collide with any value an object could convert to.
|
|
643
|
+
#define CONV_IN_PROGRESS(conv) ((conv)->r_seen)
|
|
644
|
+
|
|
645
|
+
static void conv_pin(ConvState *conv, JSValue j_val)
|
|
646
|
+
{
|
|
647
|
+
if (conv->pinned_count == conv->pinned_capacity)
|
|
648
|
+
{
|
|
649
|
+
long capacity = conv->pinned_capacity * 2;
|
|
650
|
+
JSValue *pinned = xmalloc2(capacity, sizeof(JSValue));
|
|
651
|
+
JSValue *previous = conv->pinned;
|
|
652
|
+
memcpy(pinned, previous, conv->pinned_count * sizeof(JSValue));
|
|
653
|
+
conv->pinned = pinned;
|
|
654
|
+
conv->pinned_capacity = capacity;
|
|
655
|
+
if (previous != conv->pinned_inline)
|
|
656
|
+
xfree(previous);
|
|
657
|
+
}
|
|
658
|
+
conv->pinned[conv->pinned_count++] = JS_DupValue(conv->ctx, j_val);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
static long conv_frame_push(ConvState *conv)
|
|
662
|
+
{
|
|
663
|
+
if (conv->frame_count == conv->frame_capacity)
|
|
664
|
+
{
|
|
665
|
+
long capacity = conv->frame_capacity * 2;
|
|
666
|
+
ConvFrame *frames = xmalloc2(capacity, sizeof(ConvFrame));
|
|
667
|
+
ConvFrame *previous = conv->frames;
|
|
668
|
+
memcpy(frames, previous, conv->frame_count * sizeof(ConvFrame));
|
|
669
|
+
conv->frames = frames;
|
|
670
|
+
conv->frame_capacity = capacity;
|
|
671
|
+
if (previous != conv->frames_inline)
|
|
672
|
+
xfree(previous);
|
|
673
|
+
}
|
|
674
|
+
long depth = conv->frame_count++;
|
|
675
|
+
ConvFrame *frame = CONV_FRAME(conv, depth);
|
|
676
|
+
frame->ptab = NULL;
|
|
677
|
+
frame->plen = 0;
|
|
678
|
+
frame->j_borrowed = JS_UNDEFINED;
|
|
679
|
+
frame->key = NULL;
|
|
680
|
+
return depth;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Hand a freshly acquired reference to the frame, which owns it until the walk
|
|
684
|
+
// returns it or the conversion unwinds. Returns it for the caller to use.
|
|
685
|
+
static JSValue conv_borrow(ConvState *conv, long depth, JSValue j_val)
|
|
686
|
+
{
|
|
687
|
+
CONV_FRAME(conv, depth)->j_borrowed = j_val;
|
|
688
|
+
return j_val;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
static const char *conv_borrow_key(ConvState *conv, long depth, const char *key)
|
|
692
|
+
{
|
|
693
|
+
CONV_FRAME(conv, depth)->key = key;
|
|
694
|
+
return key;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Release whatever the frame is currently holding, leaving it ready for the
|
|
698
|
+
// next iteration. Freeing JS_UNDEFINED is a no-op, so this is safe to call on a
|
|
699
|
+
// frame that borrowed only one of the two.
|
|
700
|
+
static void conv_return(ConvState *conv, long depth)
|
|
701
|
+
{
|
|
702
|
+
ConvFrame *frame = CONV_FRAME(conv, depth);
|
|
703
|
+
if (frame->key != NULL)
|
|
704
|
+
{
|
|
705
|
+
JS_FreeCString(conv->ctx, frame->key);
|
|
706
|
+
frame->key = NULL;
|
|
707
|
+
}
|
|
708
|
+
JS_FreeValue(conv->ctx, frame->j_borrowed);
|
|
709
|
+
frame->j_borrowed = JS_UNDEFINED;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
static void conv_frame_pop(ConvState *conv)
|
|
713
|
+
{
|
|
714
|
+
long depth = --conv->frame_count;
|
|
715
|
+
conv_return(conv, depth);
|
|
716
|
+
ConvFrame *frame = CONV_FRAME(conv, depth);
|
|
717
|
+
if (frame->ptab != NULL)
|
|
718
|
+
{
|
|
719
|
+
JS_FreePropertyEnum(conv->ctx, frame->ptab, frame->plen);
|
|
720
|
+
frame->ptab = NULL;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// What a value that cannot be rendered is shown as, wherever one is shown
|
|
725
|
+
// rather than reported.
|
|
726
|
+
#define QUICKJSRB_UNRENDERABLE "(unrenderable value)"
|
|
727
|
+
|
|
728
|
+
static VALUE js_array_to_rb(JSContext *ctx, JSValue j_val, ConvState *conv)
|
|
729
|
+
{
|
|
730
|
+
long depth = conv_frame_push(conv);
|
|
731
|
+
|
|
732
|
+
JSValue j_length = conv_borrow(conv, depth, JS_GetPropertyStr(ctx, j_val, "length"));
|
|
261
733
|
uint32_t length = 0;
|
|
262
734
|
JS_ToUint32(ctx, &length, j_length);
|
|
263
|
-
|
|
735
|
+
conv_return(conv, depth);
|
|
264
736
|
|
|
265
737
|
VALUE r_array = rb_ary_new_capa(length);
|
|
266
738
|
for (uint32_t i = 0; i < length; i++)
|
|
267
739
|
{
|
|
268
|
-
JSValue j_elem = JS_GetPropertyUint32(ctx, j_val, i);
|
|
269
|
-
rb_ary_push(r_array, to_rb_value_inner(ctx, j_elem,
|
|
270
|
-
|
|
740
|
+
JSValue j_elem = conv_borrow(conv, depth, JS_GetPropertyUint32(ctx, j_val, i));
|
|
741
|
+
rb_ary_push(r_array, to_rb_value_inner(ctx, j_elem, conv));
|
|
742
|
+
conv_return(conv, depth);
|
|
271
743
|
}
|
|
744
|
+
|
|
745
|
+
conv_frame_pop(conv);
|
|
272
746
|
return r_array;
|
|
273
747
|
}
|
|
274
748
|
|
|
275
|
-
static VALUE js_plain_object_to_rb(JSContext *ctx, JSValue j_val,
|
|
749
|
+
static VALUE js_plain_object_to_rb(JSContext *ctx, JSValue j_val, ConvState *conv)
|
|
276
750
|
{
|
|
751
|
+
// The frame is pushed before the table exists so that nothing can raise
|
|
752
|
+
// between acquiring it and handing it over.
|
|
753
|
+
long depth = conv_frame_push(conv);
|
|
754
|
+
|
|
277
755
|
JSPropertyEnum *ptab;
|
|
278
756
|
uint32_t plen;
|
|
279
757
|
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, j_val, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0)
|
|
758
|
+
{
|
|
759
|
+
conv_frame_pop(conv);
|
|
280
760
|
return rb_hash_new();
|
|
761
|
+
}
|
|
762
|
+
CONV_FRAME(conv, depth)->ptab = ptab;
|
|
763
|
+
CONV_FRAME(conv, depth)->plen = plen;
|
|
281
764
|
|
|
282
765
|
VALUE r_hash = rb_hash_new();
|
|
283
766
|
for (uint32_t i = 0; i < plen; i++)
|
|
284
767
|
{
|
|
285
|
-
const char *key = JS_AtomToCString(ctx, ptab[i].atom);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
768
|
+
const char *key = conv_borrow_key(conv, depth, JS_AtomToCString(ctx, ptab[i].atom));
|
|
769
|
+
// An atom is already a string, so nothing guest-written runs here and only
|
|
770
|
+
// a failed allocation answers NULL. Reporting that as the out-of-memory it
|
|
771
|
+
// is beats handing the NULL to rb_str_new2 two lines down.
|
|
772
|
+
if (key == NULL)
|
|
773
|
+
return raise_js_exception(ctx);
|
|
774
|
+
JSValue j_prop = conv_borrow(conv, depth, JS_GetProperty(ctx, j_val, ptab[i].atom));
|
|
775
|
+
rb_hash_aset(r_hash, rb_str_new2(key), to_rb_value_inner(ctx, j_prop, conv));
|
|
776
|
+
conv_return(conv, depth);
|
|
290
777
|
}
|
|
291
|
-
|
|
778
|
+
|
|
779
|
+
conv_frame_pop(conv);
|
|
292
780
|
return r_hash;
|
|
293
781
|
}
|
|
294
782
|
|
|
783
|
+
static VALUE conv_run(VALUE r_conv)
|
|
784
|
+
{
|
|
785
|
+
ConvState *conv = (ConvState *)r_conv;
|
|
786
|
+
return to_rb_value_inner(conv->ctx, conv->j_root, conv);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
static VALUE conv_release(VALUE r_conv)
|
|
790
|
+
{
|
|
791
|
+
ConvState *conv = (ConvState *)r_conv;
|
|
792
|
+
// Innermost first, mirroring the order the walk would have released them.
|
|
793
|
+
while (conv->frame_count > 0)
|
|
794
|
+
conv_frame_pop(conv);
|
|
795
|
+
if (conv->frames != conv->frames_inline)
|
|
796
|
+
xfree(conv->frames);
|
|
797
|
+
for (long i = 0; i < conv->pinned_count; i++)
|
|
798
|
+
JS_FreeValue(conv->ctx, conv->pinned[i]);
|
|
799
|
+
if (conv->pinned != conv->pinned_inline)
|
|
800
|
+
xfree(conv->pinned);
|
|
801
|
+
return Qnil;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// The straight-line sibling of ConvFrame. A block that renders a diagnostic
|
|
805
|
+
// takes several JS references at once — a name, a message, a stack — and keeps
|
|
806
|
+
// every one of them live while it builds Ruby objects from them, so there is
|
|
807
|
+
// nothing to hand back between iterations and no recursion to unwind. It gives
|
|
808
|
+
// each reference to a hold as it takes it, and the ensure that owns the hold
|
|
809
|
+
// releases the set.
|
|
810
|
+
//
|
|
811
|
+
// Raising past the frees is not hypothetical in these blocks. They run while an
|
|
812
|
+
// error is already being handled, so their Ruby allocations are the ones most
|
|
813
|
+
// likely to be short of memory, and every one of them can longjmp.
|
|
814
|
+
#define JS_HOLD_INLINE 16
|
|
815
|
+
|
|
816
|
+
typedef struct
|
|
817
|
+
{
|
|
818
|
+
enum
|
|
819
|
+
{
|
|
820
|
+
HELD_VALUE, // JSValue, released with JS_FreeValue
|
|
821
|
+
HELD_CSTRING, // JS_ToCString result, released with JS_FreeCString
|
|
822
|
+
HELD_BUFFER // xmalloc'd, released with xfree
|
|
823
|
+
} kind;
|
|
824
|
+
union
|
|
825
|
+
{
|
|
826
|
+
JSValue j_val;
|
|
827
|
+
const char *str;
|
|
828
|
+
char *buf;
|
|
829
|
+
} as;
|
|
830
|
+
} JsHeld;
|
|
831
|
+
|
|
832
|
+
typedef struct
|
|
833
|
+
{
|
|
834
|
+
JSContext *ctx;
|
|
835
|
+
JsHeld *held;
|
|
836
|
+
long count;
|
|
837
|
+
long capacity;
|
|
838
|
+
// Set when a string conversion was substituted away because the deadline
|
|
839
|
+
// fired inside it. Sticky for the life of the hold rather than per-read: the
|
|
840
|
+
// fact it records is about the evaluation, not about one property.
|
|
841
|
+
bool interrupted;
|
|
842
|
+
JsHeld held_inline[JS_HOLD_INLINE];
|
|
843
|
+
} JsHold;
|
|
844
|
+
|
|
845
|
+
static void js_hold_init(JsHold *hold, JSContext *ctx)
|
|
846
|
+
{
|
|
847
|
+
hold->ctx = ctx;
|
|
848
|
+
hold->held = hold->held_inline;
|
|
849
|
+
hold->count = 0;
|
|
850
|
+
hold->capacity = JS_HOLD_INLINE;
|
|
851
|
+
hold->interrupted = false;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// The deepest block below holds eight references at once — an exception, three
|
|
855
|
+
// property values, their three strings and a formatted headline — so the growth
|
|
856
|
+
// path is a backstop with room to spare rather than something a site relies on.
|
|
857
|
+
static JsHeld *js_hold_slot(JsHold *hold)
|
|
858
|
+
{
|
|
859
|
+
if (hold->count == hold->capacity)
|
|
860
|
+
{
|
|
861
|
+
long capacity = hold->capacity * 2;
|
|
862
|
+
JsHeld *held = xmalloc2(capacity, sizeof(JsHeld));
|
|
863
|
+
JsHeld *previous = hold->held;
|
|
864
|
+
memcpy(held, previous, hold->count * sizeof(JsHeld));
|
|
865
|
+
hold->held = held;
|
|
866
|
+
hold->capacity = capacity;
|
|
867
|
+
if (previous != hold->held_inline)
|
|
868
|
+
xfree(previous);
|
|
869
|
+
}
|
|
870
|
+
return &hold->held[hold->count++];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
static JSValue js_hold_value(JsHold *hold, JSValue j_val)
|
|
874
|
+
{
|
|
875
|
+
JsHeld *slot = js_hold_slot(hold);
|
|
876
|
+
slot->kind = HELD_VALUE;
|
|
877
|
+
slot->as.j_val = j_val;
|
|
878
|
+
return j_val;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
static const char *js_hold_own_cstring(JsHold *hold, const char *str)
|
|
882
|
+
{
|
|
883
|
+
JsHeld *slot = js_hold_slot(hold);
|
|
884
|
+
slot->kind = HELD_CSTRING;
|
|
885
|
+
slot->as.str = str;
|
|
886
|
+
return str;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Whether the evaluation has run past the budget it was armed with. QuickJS
|
|
890
|
+
// throws the interrupt as InternalError("interrupted"), but recognising it by
|
|
891
|
+
// those strings would mean two things this cannot afford: they are strings any
|
|
892
|
+
// guest can write, so a script could relabel its own error as a timeout on a VM
|
|
893
|
+
// whose budget is nowhere near spent, and reading them off the thrown object
|
|
894
|
+
// runs that object's getters — which can reach a Ruby bridge, leaving this with
|
|
895
|
+
// an exception it has nowhere to put and would pin in alive_objects for the
|
|
896
|
+
// life of the VM. The clock answers the same question and nothing guest-written
|
|
897
|
+
// runs to answer it.
|
|
898
|
+
static bool eval_budget_lapsed(VMData *data)
|
|
899
|
+
{
|
|
900
|
+
return data->eval_timer_armed && eval_elapsed_ms(data->eval_time) >= data->eval_time->limit_ms;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
bool eval_budget_lapsed_now(JSContext *ctx)
|
|
904
|
+
{
|
|
905
|
+
return eval_budget_lapsed(JS_GetContextOpaque(ctx));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// JS_ToCString converts through the value's own toString, so it answers NULL
|
|
909
|
+
// whenever that throws — a getter that raises, a Symbol, a Proxy that refuses —
|
|
910
|
+
// and not only when it runs out of memory. This keeps the NULL, for the two
|
|
911
|
+
// readers that render a value's absence differently from any string that could
|
|
912
|
+
// stand in for it.
|
|
913
|
+
static const char *js_hold_cstring_or_null(JsHold *hold, JSValue j_val)
|
|
914
|
+
{
|
|
915
|
+
const char *str = JS_ToCString(hold->ctx, j_val);
|
|
916
|
+
if (str != NULL)
|
|
917
|
+
return js_hold_own_cstring(hold, str);
|
|
918
|
+
|
|
919
|
+
// The conversion threw, and what was thrown decides who gets told. A throw
|
|
920
|
+
// carrying a Ruby exception is a bridge reporting a host failure — the user's
|
|
921
|
+
// block raising, or the ThreadError a dispose! mid-conversion owes its caller
|
|
922
|
+
// — and it has to come back out. find_ruby_error is also the only thing that
|
|
923
|
+
// takes the exception out of alive_objects, so discarding the throw instead
|
|
924
|
+
// would pin it there for the life of the VM at a rate the guest picks.
|
|
925
|
+
//
|
|
926
|
+
// Anything else is the value simply having no string form — a Symbol, a
|
|
927
|
+
// toString written to throw — and the caller substitutes for it. What is worth
|
|
928
|
+
// remembering is not the throw but the clock: a read the budget outlived says
|
|
929
|
+
// nothing about the value and everything about the evaluation.
|
|
930
|
+
JSValue j_pending = js_hold_value(hold, JS_GetException(hold->ctx));
|
|
931
|
+
VALUE r_ruby_error = find_ruby_error(hold->ctx, j_pending);
|
|
932
|
+
if (!NIL_P(r_ruby_error))
|
|
933
|
+
rb_exc_raise(r_ruby_error);
|
|
934
|
+
|
|
935
|
+
if (eval_budget_lapsed(JS_GetContextOpaque(hold->ctx)))
|
|
936
|
+
hold->interrupted = true;
|
|
937
|
+
|
|
938
|
+
return NULL;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Callers get a string they can always print, compare and hand to Ruby. The
|
|
942
|
+
// fallback is a literal and so is deliberately not held.
|
|
943
|
+
static const char *js_hold_cstring(JsHold *hold, JSValue j_val, const char *fallback)
|
|
944
|
+
{
|
|
945
|
+
const char *str = js_hold_cstring_or_null(hold, j_val);
|
|
946
|
+
return str != NULL ? str : fallback;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// snprintf reports the length it would have written, and a negative return
|
|
950
|
+
// means the format itself failed — which, handed to a malloc as `length + 1`,
|
|
951
|
+
// asks for a block the size of the address space. xmalloc raises rather than
|
|
952
|
+
// answering NULL, so the buffer is either usable or never exists.
|
|
953
|
+
static char *js_hold_format(JsHold *hold, const char *format, ...) __attribute__((format(printf, 2, 3)));
|
|
954
|
+
|
|
955
|
+
static char *js_hold_format(JsHold *hold, const char *format, ...)
|
|
956
|
+
{
|
|
957
|
+
va_list args;
|
|
958
|
+
va_start(args, format);
|
|
959
|
+
int length = vsnprintf(NULL, 0, format, args);
|
|
960
|
+
va_end(args);
|
|
961
|
+
if (length < 0)
|
|
962
|
+
length = 0;
|
|
963
|
+
|
|
964
|
+
// The slot is filled in before the allocation that could raise past it, so
|
|
965
|
+
// an out-of-memory here leaves the hold holding a NULL rather than a kind it
|
|
966
|
+
// never got round to setting.
|
|
967
|
+
JsHeld *slot = js_hold_slot(hold);
|
|
968
|
+
slot->kind = HELD_BUFFER;
|
|
969
|
+
slot->as.buf = NULL;
|
|
970
|
+
char *buf = slot->as.buf = xmalloc(length + 1);
|
|
971
|
+
|
|
972
|
+
va_start(args, format);
|
|
973
|
+
vsnprintf(buf, length + 1, format, args);
|
|
974
|
+
va_end(args);
|
|
975
|
+
return buf;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Leaves the hold empty and usable, so a block that finishes with one set and
|
|
979
|
+
// starts another can call this between them, and the ensure that calls it again
|
|
980
|
+
// afterwards finds nothing left to do.
|
|
981
|
+
static VALUE js_hold_release(VALUE r_hold)
|
|
982
|
+
{
|
|
983
|
+
JsHold *hold = (JsHold *)r_hold;
|
|
984
|
+
// Newest first, mirroring the order the block would have released them.
|
|
985
|
+
while (hold->count > 0)
|
|
986
|
+
{
|
|
987
|
+
JsHeld *slot = &hold->held[--hold->count];
|
|
988
|
+
switch (slot->kind)
|
|
989
|
+
{
|
|
990
|
+
case HELD_VALUE:
|
|
991
|
+
JS_FreeValue(hold->ctx, slot->as.j_val);
|
|
992
|
+
break;
|
|
993
|
+
case HELD_CSTRING:
|
|
994
|
+
JS_FreeCString(hold->ctx, slot->as.str);
|
|
995
|
+
break;
|
|
996
|
+
case HELD_BUFFER:
|
|
997
|
+
xfree(slot->as.buf);
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (hold->held != hold->held_inline)
|
|
1002
|
+
xfree(hold->held);
|
|
1003
|
+
hold->held = hold->held_inline;
|
|
1004
|
+
hold->capacity = JS_HOLD_INLINE;
|
|
1005
|
+
return Qnil;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Stands in wherever a value refused to convert to a string, so the diagnostic
|
|
1009
|
+
// says so rather than printing "(null)" or dereferencing it.
|
|
1010
|
+
|
|
1011
|
+
// A budget that lapsed inside one of the reads outranks whatever the script was
|
|
1012
|
+
// in the middle of saying. Rendering happens after the evaluation is over, so
|
|
1013
|
+
// unlike the log path there is no next interrupt check to report it again: let
|
|
1014
|
+
// the substituted read stand on its own and the caller is handed a plain
|
|
1015
|
+
// RuntimeError for a run that ran out of time.
|
|
1016
|
+
static VALUE r_interrupted_error(void)
|
|
1017
|
+
{
|
|
1018
|
+
VALUE r_message = rb_str_new2("Code evaluation is interrupted by the timeout or something");
|
|
1019
|
+
return rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_INTERRUPTED_ERROR), rb_intern("new"), 2, r_message, Qnil);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
static void raise_if_interrupted(JsHold *hold)
|
|
1023
|
+
{
|
|
1024
|
+
if (hold->interrupted)
|
|
1025
|
+
rb_exc_raise(r_interrupted_error());
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
struct js_exception_render
|
|
1029
|
+
{
|
|
1030
|
+
JSContext *ctx;
|
|
1031
|
+
// Whether the exception is the evaluation's own result. Only then is it news:
|
|
1032
|
+
// it gets the "Uncaught" row the console would have printed, and an
|
|
1033
|
+
// out-of-memory in it condemns the VM. An exception raised part-way through
|
|
1034
|
+
// converting a value that did return is on its way to the caller as that
|
|
1035
|
+
// call's error — telling the log listener it went uncaught would be the
|
|
1036
|
+
// opposite of what happened, and the heap it was found on is the heap of a
|
|
1037
|
+
// run that finished.
|
|
1038
|
+
bool uncaught;
|
|
1039
|
+
JsHold hold;
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
static VALUE js_exception_render_run(VALUE r_render)
|
|
1043
|
+
{
|
|
1044
|
+
struct js_exception_render *render = (struct js_exception_render *)r_render;
|
|
1045
|
+
JSContext *ctx = render->ctx;
|
|
1046
|
+
JsHold *hold = &render->hold;
|
|
1047
|
+
VMData *data = JS_GetContextOpaque(ctx);
|
|
1048
|
+
|
|
1049
|
+
JSValue j_exceptionVal = js_hold_value(hold, JS_GetException(ctx));
|
|
1050
|
+
|
|
1051
|
+
if (!JS_IsError(ctx, j_exceptionVal))
|
|
1052
|
+
{
|
|
1053
|
+
// A thrown string, number or bare object: there is no name or stack to ask
|
|
1054
|
+
// for, only what the value itself says.
|
|
1055
|
+
const char *errorMessage = js_hold_cstring(hold, j_exceptionVal, QUICKJSRB_UNRENDERABLE);
|
|
1056
|
+
if (render->uncaught)
|
|
1057
|
+
{
|
|
1058
|
+
VALUE r_headline = rb_str_new2(js_hold_format(hold, "Uncaught '%s'", errorMessage));
|
|
1059
|
+
dispatch_log(data, "error", rb_ary_new3(1, r_log_body_new(r_headline, r_headline)));
|
|
1060
|
+
}
|
|
1061
|
+
raise_if_interrupted(hold);
|
|
1062
|
+
|
|
1063
|
+
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, rb_str_new2(errorMessage), Qnil));
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
VALUE r_maybe_ruby_error = find_ruby_error(ctx, j_exceptionVal);
|
|
1067
|
+
if (!NIL_P(r_maybe_ruby_error))
|
|
1068
|
+
rb_exc_raise(r_maybe_ruby_error);
|
|
1069
|
+
// will support other errors like just returning an instance of Error
|
|
1070
|
+
|
|
1071
|
+
JSValue j_errorClassName = js_hold_value(hold, JS_GetPropertyStr(ctx, j_exceptionVal, "name"));
|
|
1072
|
+
const char *readClassName = js_hold_cstring_or_null(hold, j_errorClassName);
|
|
1073
|
+
// "Error" is a workable default for picking a Ruby class, which has to resolve
|
|
1074
|
+
// to something. It is not an answer to what the JS side called this, so it
|
|
1075
|
+
// stops here and js_name says nothing instead of naming a class nobody named.
|
|
1076
|
+
const char *errorClassName = readClassName != NULL ? readClassName : "Error";
|
|
1077
|
+
VALUE r_error_name = readClassName != NULL ? rb_str_new2(readClassName) : Qnil;
|
|
1078
|
+
|
|
1079
|
+
JSValue j_errorClassMessage = js_hold_value(hold, JS_GetPropertyStr(ctx, j_exceptionVal, "message"));
|
|
1080
|
+
const char *errorClassMessage = js_hold_cstring(hold, j_errorClassMessage, QUICKJSRB_UNRENDERABLE);
|
|
1081
|
+
|
|
1082
|
+
JSValue j_stackTrace = js_hold_value(hold, JS_GetPropertyStr(ctx, j_exceptionVal, "stack"));
|
|
1083
|
+
// Empty rather than a placeholder: this one becomes a Ruby backtrace, and an
|
|
1084
|
+
// apology reads as a frame there.
|
|
1085
|
+
const char *stackTrace = js_hold_cstring(hold, j_stackTrace, "");
|
|
1086
|
+
|
|
1087
|
+
if (render->uncaught)
|
|
1088
|
+
{
|
|
1089
|
+
VALUE r_headline = rb_str_new2(js_hold_format(hold, "Uncaught %s: %s\n%s", errorClassName, errorClassMessage, stackTrace));
|
|
1090
|
+
dispatch_log(data, "error", rb_ary_new3(1, r_log_body_new(r_headline, r_headline)));
|
|
1091
|
+
}
|
|
1092
|
+
raise_if_interrupted(hold);
|
|
1093
|
+
|
|
1094
|
+
VALUE r_error_class, r_error_message = rb_str_new2(errorClassMessage);
|
|
1095
|
+
VALUE r_backtrace = r_backtrace_from_js_stack(stackTrace);
|
|
1096
|
+
if (is_native_error_name(errorClassName))
|
|
1097
|
+
{
|
|
1098
|
+
r_error_class = QUICKJSRB_ERROR_FOR(errorClassName);
|
|
1099
|
+
}
|
|
1100
|
+
else if (strcmp(errorClassName, "InternalError") == 0 && strstr(errorClassMessage, "interrupted") != NULL)
|
|
1101
|
+
{
|
|
1102
|
+
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_INTERRUPTED_ERROR);
|
|
1103
|
+
r_error_message = rb_str_new2("Code evaluation is interrupted by the timeout or something");
|
|
1104
|
+
}
|
|
1105
|
+
else if (strcmp(errorClassName, "Quickjs::InterruptedError") == 0)
|
|
1106
|
+
{
|
|
1107
|
+
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_INTERRUPTED_ERROR);
|
|
1108
|
+
}
|
|
1109
|
+
else if (strcmp(errorClassName, "InternalError") == 0 && strstr(errorClassMessage, "out of memory") != NULL)
|
|
1110
|
+
{
|
|
1111
|
+
// Once OOM has fired, the QuickJS heap is in a state where another
|
|
1112
|
+
// throw inside the parser-error path can corrupt the shape table and
|
|
1113
|
+
// segfault. Mark the VM so further eval/call calls refuse cleanly.
|
|
1114
|
+
//
|
|
1115
|
+
// This is the one thing the uncaught path does not keep to itself. Running
|
|
1116
|
+
// out of memory is a fact about the heap, not about who was asking: a
|
|
1117
|
+
// getter on an object the evaluation successfully returned allocates on the
|
|
1118
|
+
// same heap as the evaluation did, and `({get x() { return new
|
|
1119
|
+
// Array(2_000_000).fill(0) }})` reaches OOM here with the result already in
|
|
1120
|
+
// hand. Both strings are guest-writable, so a forged InternalError condemns
|
|
1121
|
+
// the VM too — but that is reachable from any getter and always has been,
|
|
1122
|
+
// and refusing to latch here would trade a real guard for no ground.
|
|
1123
|
+
data->oom_poisoned = true;
|
|
1124
|
+
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
|
|
1125
|
+
}
|
|
1126
|
+
else
|
|
1127
|
+
{
|
|
1128
|
+
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
VALUE r_exc = rb_funcall(r_error_class, rb_intern("new"), 2, r_error_message, r_error_name);
|
|
1132
|
+
if (!NIL_P(r_backtrace))
|
|
1133
|
+
rb_funcall(r_exc, rb_intern("set_backtrace"), 1, r_backtrace);
|
|
1134
|
+
rb_exc_raise(r_exc);
|
|
1135
|
+
return Qnil; // rb_exc_raise does not return
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Renders the context's pending exception into a Ruby one and raises it. Never
|
|
1139
|
+
// returns, so the hold is released by the ensure on the way past.
|
|
1140
|
+
static VALUE raise_rendered_js_exception(JSContext *ctx, bool uncaught)
|
|
1141
|
+
{
|
|
1142
|
+
struct js_exception_render render;
|
|
1143
|
+
render.ctx = ctx;
|
|
1144
|
+
render.uncaught = uncaught;
|
|
1145
|
+
js_hold_init(&render.hold, ctx);
|
|
1146
|
+
return rb_ensure(js_exception_render_run, (VALUE)&render, js_hold_release, (VALUE)&render.hold);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// The exception nothing caught: it is the evaluation's whole result.
|
|
1150
|
+
static VALUE raise_uncaught_js_exception(JSContext *ctx)
|
|
1151
|
+
{
|
|
1152
|
+
return raise_rendered_js_exception(ctx, true);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// The exception a conversion ran into on a value that did return — a getter, a
|
|
1156
|
+
// toString, an atom that would not allocate. The caller of eval_code hears it
|
|
1157
|
+
// as that call's error and nobody else needs telling.
|
|
1158
|
+
static VALUE raise_js_exception(JSContext *ctx)
|
|
1159
|
+
{
|
|
1160
|
+
return raise_rendered_js_exception(ctx, false);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
struct js_bigint_conversion
|
|
1164
|
+
{
|
|
1165
|
+
JSContext *ctx;
|
|
1166
|
+
JSValue j_val;
|
|
1167
|
+
JsHold hold;
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
static VALUE js_bigint_conversion_run(VALUE r_conversion)
|
|
1171
|
+
{
|
|
1172
|
+
struct js_bigint_conversion *conversion = (struct js_bigint_conversion *)r_conversion;
|
|
1173
|
+
JSContext *ctx = conversion->ctx;
|
|
1174
|
+
JsHold *hold = &conversion->hold;
|
|
1175
|
+
|
|
1176
|
+
// Unlike the diagnostic blocks, this one is converting a value and has
|
|
1177
|
+
// somewhere to put a throw: a BigInt whose toString raises owes the caller
|
|
1178
|
+
// that error, not a digit string invented to stand in for it.
|
|
1179
|
+
//
|
|
1180
|
+
// The read is checked before the call, not after: calling a value that is
|
|
1181
|
+
// JS_EXCEPTION throws "not a function" over the top of the error that is
|
|
1182
|
+
// being reported, so testing afterwards reports the wrong one.
|
|
1183
|
+
JSValue j_toStringFunc = js_hold_value(hold, JS_GetPropertyStr(ctx, conversion->j_val, "toString"));
|
|
1184
|
+
if (JS_IsException(j_toStringFunc))
|
|
1185
|
+
return raise_js_exception(ctx);
|
|
1186
|
+
|
|
1187
|
+
JSValue j_strigified = js_hold_value(hold, JS_Call(ctx, j_toStringFunc, conversion->j_val, 0, NULL));
|
|
1188
|
+
if (JS_IsException(j_strigified))
|
|
1189
|
+
return raise_js_exception(ctx);
|
|
1190
|
+
|
|
1191
|
+
const char *msg = JS_ToCString(ctx, j_strigified);
|
|
1192
|
+
if (msg == NULL)
|
|
1193
|
+
return raise_js_exception(ctx);
|
|
1194
|
+
|
|
1195
|
+
return rb_funcall(rb_str_new2(js_hold_own_cstring(hold, msg)), rb_intern("to_i"), 0);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
295
1198
|
VALUE to_rb_value(JSContext *ctx, JSValue j_val)
|
|
296
1199
|
{
|
|
297
|
-
|
|
1200
|
+
// Only object graphs need the bookkeeping, and only they can recurse, so
|
|
1201
|
+
// primitives convert straight through rather than paying for the state and
|
|
1202
|
+
// the ensure. Every recursive call therefore has a non-NULL `conv`.
|
|
1203
|
+
if (JS_VALUE_GET_NORM_TAG(j_val) != JS_TAG_OBJECT)
|
|
1204
|
+
return to_rb_value_inner(ctx, j_val, NULL);
|
|
1205
|
+
|
|
1206
|
+
ConvState conv = {ctx, j_val, rb_hash_new(), NULL, 0, CONV_PINNED_INLINE};
|
|
1207
|
+
conv.pinned = conv.pinned_inline;
|
|
1208
|
+
conv.frames = conv.frames_inline;
|
|
1209
|
+
conv.frame_count = 0;
|
|
1210
|
+
conv.frame_capacity = CONV_FRAMES_INLINE;
|
|
1211
|
+
conv.substitute_unresolvable = 0;
|
|
1212
|
+
return rb_ensure(conv_run, (VALUE)&conv, conv_release, (VALUE)&conv);
|
|
298
1213
|
}
|
|
299
1214
|
|
|
300
|
-
|
|
1215
|
+
// The same conversion, for a caller that has to answer with a value: a proxy
|
|
1216
|
+
// it cannot resolve becomes the placeholder rather than an exception, at
|
|
1217
|
+
// whatever depth it is found.
|
|
1218
|
+
VALUE to_rb_value_substituting(JSContext *ctx, JSValue j_val)
|
|
1219
|
+
{
|
|
1220
|
+
if (JS_VALUE_GET_NORM_TAG(j_val) != JS_TAG_OBJECT)
|
|
1221
|
+
return to_rb_value_inner(ctx, j_val, NULL);
|
|
1222
|
+
|
|
1223
|
+
ConvState conv = {ctx, j_val, rb_hash_new(), NULL, 0, CONV_PINNED_INLINE};
|
|
1224
|
+
conv.pinned = conv.pinned_inline;
|
|
1225
|
+
conv.frames = conv.frames_inline;
|
|
1226
|
+
conv.frame_count = 0;
|
|
1227
|
+
conv.frame_capacity = CONV_FRAMES_INLINE;
|
|
1228
|
+
conv.substitute_unresolvable = 1;
|
|
1229
|
+
return rb_ensure(conv_run, (VALUE)&conv, conv_release, (VALUE)&conv);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, ConvState *conv)
|
|
301
1233
|
{
|
|
302
1234
|
switch (JS_VALUE_GET_NORM_TAG(j_val))
|
|
303
1235
|
{
|
|
@@ -341,15 +1273,61 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
|
341
1273
|
return Qnil;
|
|
342
1274
|
}
|
|
343
1275
|
|
|
1276
|
+
// Asked before anything reads the value, and before the function branch
|
|
1277
|
+
// claims it. JS_IsArray answers 1 for an array, 0 for anything else, and
|
|
1278
|
+
// -1 when it cannot resolve a proxy. A revoked one is the reachable way
|
|
1279
|
+
// there, whatever its target was; a chain of live proxies more than 1000
|
|
1280
|
+
// deep is the other, and reports the stack overflow it left pending. Both
|
|
1281
|
+
// are values this conversion cannot see through, and the guest is told the
|
|
1282
|
+
// same thing by Array.isArray on either. Read as a boolean further down, that -1 took
|
|
1283
|
+
// the array path and a value the guest is forbidden to touch came back as
|
|
1284
|
+
// an ordinary empty Array, indistinguishable from a real one.
|
|
1285
|
+
//
|
|
1286
|
+
// Here rather than at the array dispatch because JS_IsFunction stays true
|
|
1287
|
+
// for a revoked proxy over a callable target: leaving it to that branch
|
|
1288
|
+
// reported the same thing through a toString it also refuses, which is a
|
|
1289
|
+
// second way to the same answer and, in substituting mode, a way past the
|
|
1290
|
+
// substitution.
|
|
1291
|
+
int is_array = JS_IsArray(ctx, j_val);
|
|
1292
|
+
if (is_array < 0)
|
|
1293
|
+
{
|
|
1294
|
+
if (conv != NULL && conv->substitute_unresolvable)
|
|
1295
|
+
{
|
|
1296
|
+
// Taken rather than left pending: nothing downstream will report it,
|
|
1297
|
+
// and an unrelated evaluation would otherwise be handed it.
|
|
1298
|
+
quickjsrb_drain_pending(ctx);
|
|
1299
|
+
return rb_str_new2(QUICKJSRB_UNRENDERABLE);
|
|
1300
|
+
}
|
|
1301
|
+
// The throw it left is what the guest would be told, so that is what the
|
|
1302
|
+
// caller is told, rendered and taken off the context. Answering nil
|
|
1303
|
+
// instead would collide with the value a cycle already converts to.
|
|
1304
|
+
return raise_js_exception(ctx);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
344
1307
|
if (JS_IsFunction(ctx, j_val))
|
|
345
1308
|
{
|
|
346
|
-
|
|
1309
|
+
// A user-defined toString is guest code and can reach a Ruby bridge that
|
|
1310
|
+
// raises, so the frame owns the function across the call.
|
|
1311
|
+
long depth = conv_frame_push(conv);
|
|
1312
|
+
JSValue j_toStringFunc = conv_borrow(conv, depth, JS_GetPropertyStr(ctx, j_val, "toString"));
|
|
1313
|
+
// Checked before the call for the same reason as the BigInt branch:
|
|
1314
|
+
// calling JS_EXCEPTION throws "not a function" over the real error.
|
|
1315
|
+
if (JS_IsException(j_toStringFunc))
|
|
1316
|
+
return raise_js_exception(ctx);
|
|
347
1317
|
JSValue j_source = JS_Call(ctx, j_toStringFunc, j_val, 0, NULL);
|
|
348
|
-
|
|
1318
|
+
conv_return(conv, depth);
|
|
1319
|
+
conv_borrow(conv, depth, j_source);
|
|
349
1320
|
const char *source = JS_ToCString(ctx, j_source);
|
|
350
|
-
|
|
1321
|
+
// A toString that threw owes the caller that error rather than a
|
|
1322
|
+
// Quickjs::Function built from nothing — and when the throw came from a
|
|
1323
|
+
// bridge, it is the Ruby exception the bridge raised, which is how a
|
|
1324
|
+
// dispose! reached from a toString gets to report ThreadError. The frame
|
|
1325
|
+
// still holds j_source, so conv_release hands it back on the way out.
|
|
1326
|
+
if (source == NULL)
|
|
1327
|
+
return raise_js_exception(ctx);
|
|
1328
|
+
conv_borrow_key(conv, depth, source);
|
|
351
1329
|
VALUE r_source = rb_str_new2(source);
|
|
352
|
-
|
|
1330
|
+
conv_frame_pop(conv);
|
|
353
1331
|
return rb_funcall(rb_path2class("Quickjs::Function"), rb_intern("new"), 1, r_source);
|
|
354
1332
|
}
|
|
355
1333
|
|
|
@@ -365,8 +1343,17 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
|
365
1343
|
|
|
366
1344
|
// Check for Ruby object proxy (e.g., File proxy with rb_object_id on target)
|
|
367
1345
|
{
|
|
368
|
-
|
|
369
|
-
|
|
1346
|
+
// Own data only, like the other two readers: through the prototype chain
|
|
1347
|
+
// an accessor on Object.prototype answers for every object crossing back,
|
|
1348
|
+
// and one line of guest setup turns a returned Hash into whichever host
|
|
1349
|
+
// File or CryptoKey the guest already holds a handle for.
|
|
1350
|
+
JSValue j_rb_id = JS_UNDEFINED;
|
|
1351
|
+
bool has_handle = j_read_own_handle(ctx, j_val, &j_rb_id);
|
|
1352
|
+
// A Proxy trap can still answer that read and throw, and what it throws
|
|
1353
|
+
// can be a bridge reporting a host failure.
|
|
1354
|
+
if (!has_handle && JS_IsException(j_rb_id))
|
|
1355
|
+
quickjsrb_drain_pending(ctx);
|
|
1356
|
+
else if (has_handle && (JS_VALUE_GET_NORM_TAG(j_rb_id) == JS_TAG_INT || JS_VALUE_GET_NORM_TAG(j_rb_id) == JS_TAG_FLOAT64))
|
|
370
1357
|
{
|
|
371
1358
|
int64_t object_id;
|
|
372
1359
|
JS_ToInt64(ctx, &object_id, j_rb_id);
|
|
@@ -374,7 +1361,7 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
|
374
1361
|
if (object_id > 0)
|
|
375
1362
|
{
|
|
376
1363
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
377
|
-
VALUE r_obj = rb_hash_aref(data->alive_objects,
|
|
1364
|
+
VALUE r_obj = rb_hash_aref(data->alive_objects, LL2NUM(object_id));
|
|
378
1365
|
if (!NIL_P(r_obj) && !rb_obj_is_kind_of(r_obj, rb_eException))
|
|
379
1366
|
return r_obj;
|
|
380
1367
|
}
|
|
@@ -392,149 +1379,107 @@ static VALUE to_rb_value_inner(JSContext *ctx, JSValue j_val, VALUE r_visited)
|
|
|
392
1379
|
return r_maybe_file;
|
|
393
1380
|
}
|
|
394
1381
|
|
|
395
|
-
// Below this point, conversion recurses into own properties / elements
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
VALUE
|
|
401
|
-
if (
|
|
1382
|
+
// Below this point, conversion recurses into own properties / elements via
|
|
1383
|
+
// to_rb_value_inner. An object that is still on the current path is a cycle
|
|
1384
|
+
// and becomes nil; one that finished converting earlier is a shared
|
|
1385
|
+
// reference and yields the very same Ruby object again.
|
|
1386
|
+
VALUE r_key = ULL2NUM((uintptr_t)JS_VALUE_GET_PTR(j_val));
|
|
1387
|
+
VALUE r_seen = rb_hash_lookup2(conv->r_seen, r_key, Qundef);
|
|
1388
|
+
if (r_seen == CONV_IN_PROGRESS(conv))
|
|
402
1389
|
return Qnil;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
if (
|
|
1390
|
+
if (r_seen != Qundef)
|
|
1391
|
+
return r_seen;
|
|
1392
|
+
|
|
1393
|
+
conv_pin(conv, j_val);
|
|
1394
|
+
rb_hash_aset(conv->r_seen, r_key, CONV_IN_PROGRESS(conv));
|
|
1395
|
+
|
|
1396
|
+
VALUE r_result;
|
|
1397
|
+
int memoize = 1;
|
|
1398
|
+
// Asked again. The answer taken at the top of this case is stale by now:
|
|
1399
|
+
// the handle read and the File check both run guest traps, and a proxy
|
|
1400
|
+
// that revokes itself from one of them was resolvable when we asked and is
|
|
1401
|
+
// not any more. Reading the old answer here put that value back on the
|
|
1402
|
+
// array path, which is the fabrication this is all about.
|
|
1403
|
+
is_array = JS_IsArray(ctx, j_val);
|
|
1404
|
+
if (is_array < 0)
|
|
418
1405
|
{
|
|
419
|
-
|
|
420
|
-
JS_FreeValue(ctx, j_toJSON);
|
|
421
|
-
VALUE r_result = to_rb_value_inner(ctx, j_jsonValue, r_visited);
|
|
422
|
-
JS_FreeValue(ctx, j_jsonValue);
|
|
423
|
-
return r_result;
|
|
424
|
-
}
|
|
425
|
-
JS_FreeValue(ctx, j_toJSON);
|
|
426
|
-
return js_plain_object_to_rb(ctx, j_val, r_visited);
|
|
427
|
-
}
|
|
428
|
-
case JS_TAG_NULL:
|
|
429
|
-
return Qnil;
|
|
430
|
-
case JS_TAG_UNDEFINED:
|
|
431
|
-
return QUICKJSRB_SYM(undefinedId);
|
|
432
|
-
case JS_TAG_EXCEPTION:
|
|
433
|
-
{
|
|
434
|
-
JSValue j_exceptionVal = JS_GetException(ctx);
|
|
435
|
-
if (JS_IsError(ctx, j_exceptionVal))
|
|
436
|
-
{
|
|
437
|
-
VALUE r_maybe_ruby_error = find_ruby_error(ctx, j_exceptionVal);
|
|
438
|
-
if (!NIL_P(r_maybe_ruby_error))
|
|
1406
|
+
if (conv->substitute_unresolvable)
|
|
439
1407
|
{
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
1408
|
+
quickjsrb_drain_pending(ctx);
|
|
1409
|
+
// The in-progress mark goes with it: this value never entered the walk
|
|
1410
|
+
// it was marked into, and leaving the mark would make a second
|
|
1411
|
+
// occurrence answer the cycle check and convert to nil.
|
|
1412
|
+
rb_hash_delete(conv->r_seen, r_key);
|
|
1413
|
+
return rb_str_new2(QUICKJSRB_UNRENDERABLE);
|
|
443
1414
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
VALUE r_error_name = rb_str_new2(errorClassName);
|
|
465
|
-
VALUE r_backtrace = r_backtrace_from_js_stack(stackTrace);
|
|
466
|
-
if (is_native_error_name(errorClassName))
|
|
467
|
-
{
|
|
468
|
-
r_error_class = QUICKJSRB_ERROR_FOR(errorClassName);
|
|
469
|
-
}
|
|
470
|
-
else if (strcmp(errorClassName, "InternalError") == 0 && strstr(errorClassMessage, "interrupted") != NULL)
|
|
471
|
-
{
|
|
472
|
-
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_INTERRUPTED_ERROR);
|
|
473
|
-
r_error_message = rb_str_new2("Code evaluation is interrupted by the timeout or something");
|
|
474
|
-
}
|
|
475
|
-
else if (strcmp(errorClassName, "Quickjs::InterruptedError") == 0)
|
|
476
|
-
{
|
|
477
|
-
r_error_class = QUICKJSRB_ERROR_FOR(QUICKJSRB_INTERRUPTED_ERROR);
|
|
478
|
-
}
|
|
479
|
-
else if (strcmp(errorClassName, "InternalError") == 0 && strstr(errorClassMessage, "out of memory") != NULL)
|
|
1415
|
+
return raise_js_exception(ctx);
|
|
1416
|
+
}
|
|
1417
|
+
if (is_array > 0)
|
|
1418
|
+
{
|
|
1419
|
+
r_result = js_array_to_rb(ctx, j_val, conv);
|
|
1420
|
+
}
|
|
1421
|
+
else if (js_is_plain_object(ctx, j_val))
|
|
1422
|
+
{
|
|
1423
|
+
r_result = js_plain_object_to_rb(ctx, j_val, conv);
|
|
1424
|
+
}
|
|
1425
|
+
else
|
|
1426
|
+
{
|
|
1427
|
+
// Non-plain objects (Date, RegExp, Map, class instances, etc.).
|
|
1428
|
+
// If the object opts in to a JSON representation via toJSON (e.g. Date),
|
|
1429
|
+
// honour it — recurse on the returned value. Otherwise dump own enumerable
|
|
1430
|
+
// string-keyed properties; this is faster than the JSON round-trip and
|
|
1431
|
+
// preserves `undefined` values nested inside class instances.
|
|
1432
|
+
long depth = conv_frame_push(conv);
|
|
1433
|
+
JSValue j_toJSON = conv_borrow(conv, depth, JS_GetPropertyStr(ctx, j_val, "toJSON"));
|
|
1434
|
+
if (JS_IsFunction(ctx, j_toJSON))
|
|
480
1435
|
{
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
1436
|
+
// toJSON is guest code and can reach a Ruby bridge that raises, so the
|
|
1437
|
+
// frame holds the function across the call and the result across the
|
|
1438
|
+
// recursion.
|
|
1439
|
+
JSValue j_jsonValue = JS_Call(ctx, j_toJSON, j_val, 0, NULL);
|
|
1440
|
+
conv_return(conv, depth);
|
|
1441
|
+
conv_borrow(conv, depth, j_jsonValue);
|
|
1442
|
+
r_result = to_rb_value_inner(ctx, j_jsonValue, conv);
|
|
1443
|
+
conv_return(conv, depth);
|
|
1444
|
+
// A toJSON representation is recomputed for every occurrence instead of
|
|
1445
|
+
// being shared. It is the object's stand-in value rather than a
|
|
1446
|
+
// container this conversion built, so memoizing it would hand out the
|
|
1447
|
+
// same mutable String for a Date reached twice, and would claim
|
|
1448
|
+
// identity between two distinct JS objects whose toJSON returns the
|
|
1449
|
+
// same thing. JSON.stringify also calls toJSON once per occurrence.
|
|
1450
|
+
memoize = 0;
|
|
486
1451
|
}
|
|
487
1452
|
else
|
|
488
1453
|
{
|
|
489
|
-
|
|
1454
|
+
conv_return(conv, depth);
|
|
1455
|
+
r_result = js_plain_object_to_rb(ctx, j_val, conv);
|
|
490
1456
|
}
|
|
491
|
-
|
|
492
|
-
JS_FreeValue(ctx, j_errorClassName);
|
|
493
|
-
JS_FreeValue(ctx, j_stackTrace);
|
|
494
|
-
JS_FreeCString(ctx, stackTrace);
|
|
495
|
-
JS_FreeCString(ctx, errorClassName);
|
|
496
|
-
JS_FreeCString(ctx, errorClassMessage);
|
|
497
|
-
JS_FreeValue(ctx, j_exceptionVal);
|
|
498
|
-
|
|
499
|
-
VALUE r_exc = rb_funcall(r_error_class, rb_intern("new"), 2, r_error_message, r_error_name);
|
|
500
|
-
if (!NIL_P(r_backtrace))
|
|
501
|
-
rb_funcall(r_exc, rb_intern("set_backtrace"), 1, r_backtrace);
|
|
502
|
-
rb_exc_raise(r_exc);
|
|
1457
|
+
conv_frame_pop(conv);
|
|
503
1458
|
}
|
|
504
|
-
else // exception without Error object
|
|
505
|
-
{
|
|
506
|
-
const char *errorMessage = JS_ToCString(ctx, j_exceptionVal);
|
|
507
|
-
const char *headlineTemplate = "Uncaught '%s'";
|
|
508
|
-
int length = snprintf(NULL, 0, headlineTemplate, errorMessage);
|
|
509
|
-
char *headline = (char *)malloc(length + 1);
|
|
510
|
-
snprintf(headline, length + 1, headlineTemplate, errorMessage);
|
|
511
1459
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
VALUE r_error_message = rb_sprintf("%s", errorMessage);
|
|
519
|
-
JS_FreeCString(ctx, errorMessage);
|
|
520
|
-
JS_FreeValue(ctx, j_exceptionVal);
|
|
521
|
-
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
|
|
522
|
-
}
|
|
523
|
-
return Qnil;
|
|
1460
|
+
if (memoize)
|
|
1461
|
+
rb_hash_aset(conv->r_seen, r_key, r_result);
|
|
1462
|
+
else
|
|
1463
|
+
rb_hash_delete(conv->r_seen, r_key);
|
|
1464
|
+
return r_result;
|
|
524
1465
|
}
|
|
1466
|
+
case JS_TAG_NULL:
|
|
1467
|
+
return Qnil;
|
|
1468
|
+
case JS_TAG_UNDEFINED:
|
|
1469
|
+
return QUICKJSRB_SYM(undefinedId);
|
|
1470
|
+
case JS_TAG_EXCEPTION:
|
|
1471
|
+
// conv is NULL only for the value to_rb_value was handed, which is the
|
|
1472
|
+
// evaluation's result; anything reached through a property read is part-way
|
|
1473
|
+
// through a walk and belongs to the caller, not to the console.
|
|
1474
|
+
return conv == NULL ? raise_uncaught_js_exception(ctx) : raise_js_exception(ctx);
|
|
525
1475
|
case JS_TAG_BIG_INT:
|
|
526
1476
|
case JS_TAG_SHORT_BIG_INT:
|
|
527
1477
|
{
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
VALUE
|
|
533
|
-
JS_FreeValue(ctx, j_toStringFunc);
|
|
534
|
-
JS_FreeValue(ctx, j_strigified);
|
|
535
|
-
JS_FreeCString(ctx, msg);
|
|
536
|
-
|
|
537
|
-
return rb_funcall(r_str, rb_intern("to_i"), 0);
|
|
1478
|
+
struct js_bigint_conversion conversion;
|
|
1479
|
+
conversion.ctx = ctx;
|
|
1480
|
+
conversion.j_val = j_val;
|
|
1481
|
+
js_hold_init(&conversion.hold, ctx);
|
|
1482
|
+
return rb_ensure(js_bigint_conversion_run, (VALUE)&conversion, js_hold_release, (VALUE)&conversion.hold);
|
|
538
1483
|
}
|
|
539
1484
|
case JS_TAG_SYMBOL:
|
|
540
1485
|
default:
|
|
@@ -584,6 +1529,14 @@ static char *quickjsrb_module_normalize(JSContext *ctx, const char *base_name, c
|
|
|
584
1529
|
if (!NIL_P(r_cached_canonical))
|
|
585
1530
|
return js_strdup(ctx, StringValueCStr(r_cached_canonical));
|
|
586
1531
|
|
|
1532
|
+
// A preloaded name is already in ctx->loaded_modules, so js_find_loaded_module
|
|
1533
|
+
// will hand back that module the moment we return the name. Resolving it here
|
|
1534
|
+
// keeps the user's loader out of it entirely: it is never asked for a module
|
|
1535
|
+
// the VM was constructed with, the way a browser's module map short-circuits
|
|
1536
|
+
// a fetch.
|
|
1537
|
+
if (RTEST(rb_hash_aref(data->preloaded_module_names, r_specifier)))
|
|
1538
|
+
return js_strdup(ctx, StringValueCStr(r_specifier));
|
|
1539
|
+
|
|
587
1540
|
struct module_loader_call_args args = {data->module_loader, r_specifier, r_importer};
|
|
588
1541
|
int state;
|
|
589
1542
|
VALUE r_return = rb_protect(r_module_loader_call, (VALUE)&args, &state);
|
|
@@ -592,6 +1545,9 @@ static char *quickjsrb_module_normalize(JSContext *ctx, const char *base_name, c
|
|
|
592
1545
|
VALUE r_error = rb_errinfo();
|
|
593
1546
|
rb_set_errinfo(Qnil);
|
|
594
1547
|
JSValue j_error = j_error_from_ruby_error(ctx, r_error);
|
|
1548
|
+
// JS_Throw would replace the throw that made this fail with the sentinel.
|
|
1549
|
+
if (JS_IsException(j_error))
|
|
1550
|
+
return NULL;
|
|
595
1551
|
JS_Throw(ctx, j_error);
|
|
596
1552
|
return NULL;
|
|
597
1553
|
}
|
|
@@ -612,14 +1568,18 @@ static char *quickjsrb_module_normalize(JSContext *ctx, const char *base_name, c
|
|
|
612
1568
|
{
|
|
613
1569
|
r_source = rb_hash_aref(r_return, ID2SYM(rb_intern("code")));
|
|
614
1570
|
r_canonical = rb_hash_aref(r_return, ID2SYM(rb_intern("as")));
|
|
615
|
-
if (!RB_TYPE_P(
|
|
1571
|
+
if (!RB_TYPE_P(r_canonical, T_STRING))
|
|
616
1572
|
{
|
|
617
|
-
JS_ThrowTypeError(ctx, "module loader Hash must include
|
|
1573
|
+
JS_ThrowTypeError(ctx, "module loader Hash must include as: (String, the canonical module name)");
|
|
618
1574
|
return NULL;
|
|
619
1575
|
}
|
|
620
|
-
|
|
1576
|
+
// `code:` is optional: omitting it makes the Hash a pure redirect, "resolve
|
|
1577
|
+
// this specifier to `as:`", which is meaningful exactly when that module is
|
|
1578
|
+
// already in the map and has no source left to provide. Requiring it would
|
|
1579
|
+
// force importmap-style loaders to invent a source value nothing reads.
|
|
1580
|
+
if (!NIL_P(r_source) && !RB_TYPE_P(r_source, T_STRING))
|
|
621
1581
|
{
|
|
622
|
-
JS_ThrowTypeError(ctx, "module loader Hash must
|
|
1582
|
+
JS_ThrowTypeError(ctx, "module loader Hash code: must be a String (the module source), or omitted to redirect to as:");
|
|
623
1583
|
return NULL;
|
|
624
1584
|
}
|
|
625
1585
|
}
|
|
@@ -630,7 +1590,13 @@ static char *quickjsrb_module_normalize(JSContext *ctx, const char *base_name, c
|
|
|
630
1590
|
return NULL;
|
|
631
1591
|
}
|
|
632
1592
|
|
|
633
|
-
|
|
1593
|
+
// The loader can also land on a preloaded module the long way round: an
|
|
1594
|
+
// importmap-style scope maps a bare specifier to a canonical that was
|
|
1595
|
+
// preloaded. QuickJS finds it in ctx->loaded_modules and never calls the
|
|
1596
|
+
// load hook, so stashing the source it just handed us would leave an entry
|
|
1597
|
+
// nothing ever clears.
|
|
1598
|
+
if (!NIL_P(r_source) && !RTEST(rb_hash_aref(data->preloaded_module_names, r_canonical)))
|
|
1599
|
+
rb_hash_aset(data->module_source_cache, r_canonical, r_source);
|
|
634
1600
|
rb_hash_aset(data->module_resolution_cache, r_key, r_canonical);
|
|
635
1601
|
|
|
636
1602
|
return js_strdup(ctx, StringValueCStr(r_canonical));
|
|
@@ -644,8 +1610,12 @@ static JSModuleDef *quickjsrb_module_loader(JSContext *ctx, const char *module_n
|
|
|
644
1610
|
VALUE r_source = rb_hash_aref(data->module_source_cache, r_canonical);
|
|
645
1611
|
if (NIL_P(r_source))
|
|
646
1612
|
{
|
|
647
|
-
//
|
|
648
|
-
|
|
1613
|
+
// Reachable through a redirect: a Hash without `code:` resolves a specifier
|
|
1614
|
+
// onto another canonical without providing any source, which only works if
|
|
1615
|
+
// that module is already loaded — and if it were, QuickJS would have found
|
|
1616
|
+
// it and never called this hook. For every other path normalize stashes a
|
|
1617
|
+
// source on the way past, so arriving here means the redirect dangled.
|
|
1618
|
+
JS_ThrowReferenceError(ctx, "module loader has no source for '%s': a Hash without code: redirects to an already-loaded module, and this one isn't loaded", module_name);
|
|
649
1619
|
return NULL;
|
|
650
1620
|
}
|
|
651
1621
|
// QuickJS won't call load again for this canonical — its own module cache
|
|
@@ -675,46 +1645,83 @@ static void register_module_loader_funcs(VMData *data)
|
|
|
675
1645
|
JS_SetModuleLoaderFunc2(runtime, quickjsrb_module_normalize, quickjsrb_module_loader, js_module_check_attributes, NULL);
|
|
676
1646
|
}
|
|
677
1647
|
|
|
678
|
-
|
|
1648
|
+
struct js_reason_conversion
|
|
1649
|
+
{
|
|
1650
|
+
JSContext *ctx;
|
|
1651
|
+
JSValueConst j_reason;
|
|
1652
|
+
int64_t *peek_handle;
|
|
1653
|
+
JsHold hold;
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
static VALUE js_reason_conversion_run(VALUE r_conversion)
|
|
679
1657
|
{
|
|
1658
|
+
struct js_reason_conversion *conversion = (struct js_reason_conversion *)r_conversion;
|
|
1659
|
+
JSContext *ctx = conversion->ctx;
|
|
1660
|
+
JSValueConst j_reason = conversion->j_reason;
|
|
1661
|
+
JsHold *hold = &conversion->hold;
|
|
1662
|
+
|
|
680
1663
|
if (JS_IsError(ctx, j_reason))
|
|
681
1664
|
{
|
|
682
|
-
VALUE r_maybe_ruby_error =
|
|
1665
|
+
VALUE r_maybe_ruby_error = find_ruby_error_peeking(ctx, j_reason, conversion->peek_handle);
|
|
683
1666
|
if (!NIL_P(r_maybe_ruby_error))
|
|
684
1667
|
return r_maybe_ruby_error;
|
|
685
1668
|
|
|
686
|
-
JSValue j_name = JS_GetPropertyStr(ctx, j_reason, "name");
|
|
687
|
-
JSValue j_message = JS_GetPropertyStr(ctx, j_reason, "message");
|
|
688
|
-
JSValue j_stack = JS_GetPropertyStr(ctx, j_reason, "stack");
|
|
689
|
-
const char *
|
|
690
|
-
const char *
|
|
691
|
-
const char *
|
|
1669
|
+
JSValue j_name = js_hold_value(hold, JS_GetPropertyStr(ctx, j_reason, "name"));
|
|
1670
|
+
JSValue j_message = js_hold_value(hold, JS_GetPropertyStr(ctx, j_reason, "message"));
|
|
1671
|
+
JSValue j_stack = js_hold_value(hold, JS_GetPropertyStr(ctx, j_reason, "stack"));
|
|
1672
|
+
const char *readName = js_hold_cstring_or_null(hold, j_name);
|
|
1673
|
+
const char *name = readName != NULL ? readName : "Error";
|
|
1674
|
+
const char *message = js_hold_cstring(hold, j_message, QUICKJSRB_UNRENDERABLE);
|
|
1675
|
+
const char *stack = js_hold_cstring(hold, j_stack, "");
|
|
692
1676
|
|
|
693
1677
|
VALUE r_class = is_native_error_name(name)
|
|
694
1678
|
? QUICKJSRB_ERROR_FOR(name)
|
|
695
1679
|
: QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR);
|
|
1680
|
+
// js_name is left nil rather than given the default the class selection
|
|
1681
|
+
// needed, for the reason the renderer spells out: a name nobody could read
|
|
1682
|
+
// is not "Error".
|
|
696
1683
|
VALUE r_exc = rb_funcall(r_class, rb_intern("new"), 2,
|
|
697
|
-
rb_str_new2(message), rb_str_new2(
|
|
1684
|
+
rb_str_new2(message), readName != NULL ? rb_str_new2(readName) : Qnil);
|
|
698
1685
|
VALUE r_backtrace = r_backtrace_from_js_stack(stack);
|
|
699
1686
|
if (!NIL_P(r_backtrace))
|
|
700
1687
|
rb_funcall(r_exc, rb_intern("set_backtrace"), 1, r_backtrace);
|
|
701
1688
|
|
|
702
|
-
JS_FreeCString(ctx, name);
|
|
703
|
-
JS_FreeCString(ctx, message);
|
|
704
|
-
if (stack)
|
|
705
|
-
JS_FreeCString(ctx, stack);
|
|
706
|
-
JS_FreeValue(ctx, j_name);
|
|
707
|
-
JS_FreeValue(ctx, j_message);
|
|
708
|
-
JS_FreeValue(ctx, j_stack);
|
|
709
1689
|
return r_exc;
|
|
710
1690
|
}
|
|
711
1691
|
|
|
712
|
-
const char *str =
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1692
|
+
const char *str = js_hold_cstring(hold, j_reason, "(non-stringifiable rejection)");
|
|
1693
|
+
return rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"),
|
|
1694
|
+
2, rb_str_new2(str), Qnil);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// The reason belongs to the tracker, which is a QuickJS host callback, so this
|
|
1698
|
+
// never raises out: the caller runs it under rb_protect and the hold gives the
|
|
1699
|
+
// references back on the way past.
|
|
1700
|
+
static VALUE r_exception_from_js_reason(JSContext *ctx, JSValueConst j_reason, int64_t *peek_handle)
|
|
1701
|
+
{
|
|
1702
|
+
struct js_reason_conversion conversion;
|
|
1703
|
+
conversion.ctx = ctx;
|
|
1704
|
+
conversion.j_reason = j_reason;
|
|
1705
|
+
conversion.peek_handle = peek_handle;
|
|
1706
|
+
js_hold_init(&conversion.hold, ctx);
|
|
1707
|
+
VALUE r_exc = rb_ensure(js_reason_conversion_run, (VALUE)&conversion, js_hold_release, (VALUE)&conversion.hold);
|
|
1708
|
+
// The flag outlives the release, which is why it is sticky: a budget that
|
|
1709
|
+
// lapsed while the reason was read is what the listener hears about, since
|
|
1710
|
+
// the tracker cannot raise out and there may be no next interrupt check.
|
|
1711
|
+
return conversion.hold.interrupted ? r_interrupted_error() : r_exc;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
struct rejection_reason_args
|
|
1715
|
+
{
|
|
1716
|
+
JSContext *ctx;
|
|
1717
|
+
JSValueConst j_reason;
|
|
1718
|
+
int64_t *peek_handle;
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
static VALUE r_rejection_reason(VALUE r_args_val)
|
|
1722
|
+
{
|
|
1723
|
+
struct rejection_reason_args *args = (struct rejection_reason_args *)r_args_val;
|
|
1724
|
+
return r_exception_from_js_reason(args->ctx, args->j_reason, args->peek_handle);
|
|
718
1725
|
}
|
|
719
1726
|
|
|
720
1727
|
struct rejection_call_args
|
|
@@ -729,27 +1736,243 @@ static VALUE r_rejection_call(VALUE r_args_val)
|
|
|
729
1736
|
return rb_funcall(args->proc, rb_intern("call"), 1, args->r_reason);
|
|
730
1737
|
}
|
|
731
1738
|
|
|
1739
|
+
static uint32_t rejection_list_hash(JSValueConst promise, uint32_t mask)
|
|
1740
|
+
{
|
|
1741
|
+
uint64_t p = (uint64_t)(uintptr_t)JS_VALUE_GET_PTR(promise);
|
|
1742
|
+
return (uint32_t)((p * 0x9E3779B97F4A7C15ULL) >> 32) & mask;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
static void rejection_list_index_insert(RejectionList *list, uint32_t i)
|
|
1746
|
+
{
|
|
1747
|
+
uint32_t mask = list->cap * 2 - 1;
|
|
1748
|
+
uint32_t h = rejection_list_hash(list->items[i].promise, mask);
|
|
1749
|
+
while (list->index[h] != 0)
|
|
1750
|
+
h = (h + 1) & mask;
|
|
1751
|
+
list->index[h] = i + 1;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Compacts the live items into new storage and reindexes them.
|
|
1755
|
+
static bool rejection_list_rebuild(JSContext *ctx, RejectionList *list, uint32_t cap)
|
|
1756
|
+
{
|
|
1757
|
+
RejectionEntry *items = js_malloc(ctx, sizeof(RejectionEntry) * cap);
|
|
1758
|
+
uint32_t *index = js_mallocz(ctx, sizeof(uint32_t) * cap * 2);
|
|
1759
|
+
if (items == NULL || index == NULL)
|
|
1760
|
+
{
|
|
1761
|
+
js_free(ctx, items);
|
|
1762
|
+
js_free(ctx, index);
|
|
1763
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
uint32_t n = 0;
|
|
1767
|
+
for (uint32_t i = 0; i < list->count; i++)
|
|
1768
|
+
if (!JS_IsUndefined(list->items[i].promise))
|
|
1769
|
+
items[n++] = list->items[i];
|
|
1770
|
+
js_free(ctx, list->items);
|
|
1771
|
+
js_free(ctx, list->index);
|
|
1772
|
+
list->items = items;
|
|
1773
|
+
list->index = index;
|
|
1774
|
+
list->count = n;
|
|
1775
|
+
list->cap = cap;
|
|
1776
|
+
for (uint32_t i = 0; i < n; i++)
|
|
1777
|
+
rejection_list_index_insert(list, i);
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
static void rejection_list_add(JSContext *ctx, RejectionList *list, JSValueConst promise, VALUE reason,
|
|
1782
|
+
int64_t error_handle)
|
|
1783
|
+
{
|
|
1784
|
+
if (list->count == list->cap)
|
|
1785
|
+
{
|
|
1786
|
+
uint32_t cap = list->cap == 0 ? 8 : list->live * 2 > list->cap ? list->cap * 2 : list->cap;
|
|
1787
|
+
if (cap > (UINT32_MAX >> 2) || !rejection_list_rebuild(ctx, list, cap))
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
uint32_t i = list->count++;
|
|
1791
|
+
list->items[i] = (RejectionEntry){JS_DupValue(ctx, promise), reason, error_handle};
|
|
1792
|
+
list->live++;
|
|
1793
|
+
rejection_list_index_insert(list, i);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// The index slot holding promise, or -1.
|
|
1797
|
+
static int64_t rejection_list_slot(const RejectionList *list, JSValueConst promise)
|
|
1798
|
+
{
|
|
1799
|
+
if (list == NULL || list->cap == 0)
|
|
1800
|
+
return -1;
|
|
1801
|
+
uint32_t mask = list->cap * 2 - 1;
|
|
1802
|
+
for (uint32_t h = rejection_list_hash(promise, mask); list->index[h] != 0; h = (h + 1) & mask)
|
|
1803
|
+
{
|
|
1804
|
+
if (list->index[h] != UINT32_MAX &&
|
|
1805
|
+
JS_VALUE_GET_PTR(list->items[list->index[h] - 1].promise) == JS_VALUE_GET_PTR(promise))
|
|
1806
|
+
return h;
|
|
1807
|
+
}
|
|
1808
|
+
return -1;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
static bool rejection_list_remove(JSContext *ctx, RejectionList *list, JSValueConst promise)
|
|
1812
|
+
{
|
|
1813
|
+
int64_t h = rejection_list_slot(list, promise);
|
|
1814
|
+
if (h < 0)
|
|
1815
|
+
return false;
|
|
1816
|
+
uint32_t i = list->index[h] - 1;
|
|
1817
|
+
JS_FreeValue(ctx, list->items[i].promise);
|
|
1818
|
+
list->items[i] = (RejectionEntry){JS_UNDEFINED, Qnil, 0};
|
|
1819
|
+
list->index[h] = UINT32_MAX;
|
|
1820
|
+
list->live--;
|
|
1821
|
+
return true;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// Moves the oldest n live entries into out; the rest stay, in order.
|
|
1825
|
+
static void rejection_list_split_oldest(JSContext *ctx, RejectionList *list, uint32_t n, RejectionList *out)
|
|
1826
|
+
{
|
|
1827
|
+
if (n >= list->live)
|
|
1828
|
+
{
|
|
1829
|
+
*out = *list;
|
|
1830
|
+
memset(list, 0, sizeof(*list));
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
memset(out, 0, sizeof(*out));
|
|
1834
|
+
for (uint32_t i = 0; i < list->count && out->live < n; i++)
|
|
1835
|
+
{
|
|
1836
|
+
RejectionEntry entry = list->items[i];
|
|
1837
|
+
if (JS_IsUndefined(entry.promise))
|
|
1838
|
+
continue;
|
|
1839
|
+
uint32_t before = out->live;
|
|
1840
|
+
rejection_list_add(ctx, out, entry.promise, entry.reason, entry.error_handle);
|
|
1841
|
+
if (out->live == before)
|
|
1842
|
+
break; // out of memory: the rest stays pending
|
|
1843
|
+
rejection_list_remove(ctx, list, entry.promise);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Also looks in the batch being notified, so a promise handled by then is skipped.
|
|
1848
|
+
static void pending_rejections_remove(VMData *data, JSValueConst promise)
|
|
1849
|
+
{
|
|
1850
|
+
if (JS_VALUE_GET_TAG(promise) != JS_TAG_OBJECT)
|
|
1851
|
+
return;
|
|
1852
|
+
if (!rejection_list_remove(data->context, &data->pending_rejections, promise))
|
|
1853
|
+
rejection_list_remove(data->context, data->notifying_rejections, promise);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// The reason is read here, inside the JS that rejected and on its clock, as
|
|
1857
|
+
// it always was; only the delivery waits, since a handler may still be
|
|
1858
|
+
// attached later in the same checkpoint.
|
|
732
1859
|
static void quickjsrb_promise_rejection_tracker(
|
|
733
1860
|
JSContext *ctx, JSValueConst promise, JSValueConst reason,
|
|
734
1861
|
JS_BOOL is_handled, void *opaque)
|
|
735
1862
|
{
|
|
736
|
-
if (is_handled)
|
|
737
|
-
return;
|
|
738
|
-
|
|
739
1863
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
740
1864
|
if (NIL_P(data->on_unhandled_rejection))
|
|
741
1865
|
return;
|
|
742
1866
|
|
|
743
|
-
|
|
744
|
-
|
|
1867
|
+
if (is_handled)
|
|
1868
|
+
{
|
|
1869
|
+
pending_rejections_remove(data, promise);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// Recorded before the reason is read, so a rejection the read makes comes
|
|
1874
|
+
// after it, and a handler the read attaches removes it.
|
|
1875
|
+
rejection_list_add(ctx, &data->pending_rejections, promise, Qnil, 0);
|
|
1876
|
+
|
|
1877
|
+
// A bridge error raised while reading the reason becomes the reason.
|
|
1878
|
+
int64_t error_handle = 0;
|
|
1879
|
+
struct rejection_reason_args reason_args = {ctx, reason, &error_handle};
|
|
745
1880
|
int state;
|
|
746
|
-
rb_protect(
|
|
1881
|
+
VALUE r_reason = rb_protect(r_rejection_reason, (VALUE)&reason_args, &state);
|
|
747
1882
|
if (state)
|
|
748
1883
|
{
|
|
749
|
-
|
|
750
|
-
// a raise inside the user's tracker has to be dropped on the floor.
|
|
1884
|
+
VALUE r_raised = rb_errinfo();
|
|
751
1885
|
rb_set_errinfo(Qnil);
|
|
1886
|
+
// A Ruby `throw` leaves no exception to report.
|
|
1887
|
+
r_reason = rb_obj_is_kind_of(r_raised, rb_eException) ? r_raised : Qnil;
|
|
1888
|
+
error_handle = 0;
|
|
752
1889
|
}
|
|
1890
|
+
int64_t h = rejection_list_slot(&data->pending_rejections, promise);
|
|
1891
|
+
if (h < 0)
|
|
1892
|
+
return;
|
|
1893
|
+
if (NIL_P(r_reason))
|
|
1894
|
+
{
|
|
1895
|
+
rejection_list_remove(ctx, &data->pending_rejections, promise);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
RejectionEntry *entry = &data->pending_rejections.items[data->pending_rejections.index[h] - 1];
|
|
1899
|
+
entry->reason = r_reason;
|
|
1900
|
+
entry->error_handle = error_handle;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// A promise the host awaits is handled by the host: its rejection is raised.
|
|
1904
|
+
static JSValue quickjsrb_host_await(JSContext *ctx, JSValue promise)
|
|
1905
|
+
{
|
|
1906
|
+
JSValue held = JS_DupValue(ctx, promise);
|
|
1907
|
+
JSValue ret = js_std_await(ctx, promise); // frees promise
|
|
1908
|
+
pending_rejections_remove(JS_GetContextOpaque(ctx), held);
|
|
1909
|
+
JS_FreeValue(ctx, held);
|
|
1910
|
+
return ret;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
static void notify_rejection_batch(VMData *data, RejectionList *batch)
|
|
1914
|
+
{
|
|
1915
|
+
data->notifying_rejections = batch;
|
|
1916
|
+
for (uint32_t i = 0; i < batch->count; i++)
|
|
1917
|
+
{
|
|
1918
|
+
RejectionEntry entry = batch->items[i];
|
|
1919
|
+
if (JS_IsUndefined(entry.promise))
|
|
1920
|
+
continue;
|
|
1921
|
+
if (entry.error_handle != 0)
|
|
1922
|
+
release_peeked_ruby_error(data, entry.error_handle, entry.reason);
|
|
1923
|
+
|
|
1924
|
+
struct rejection_call_args call_args = {data->on_unhandled_rejection, entry.reason};
|
|
1925
|
+
int state;
|
|
1926
|
+
rb_protect(r_rejection_call, (VALUE)&call_args, &state);
|
|
1927
|
+
if (state)
|
|
1928
|
+
rb_set_errinfo(Qnil);
|
|
1929
|
+
}
|
|
1930
|
+
data->notifying_rejections = NULL;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// The rest stay pending, ahead of any made while notifying.
|
|
1934
|
+
static void quickjsrb_notify_oldest_rejections(VMData *data, uint32_t n)
|
|
1935
|
+
{
|
|
1936
|
+
if (NIL_P(data->on_unhandled_rejection) || n == 0)
|
|
1937
|
+
return;
|
|
1938
|
+
// $! is neither shown to the handler nor lost to its errors. A throw or a
|
|
1939
|
+
// kill in flight has no exception to put back, so its rejections wait.
|
|
1940
|
+
VALUE r_errinfo = rb_errinfo();
|
|
1941
|
+
if (!NIL_P(r_errinfo) && !rb_obj_is_kind_of(r_errinfo, rb_eException))
|
|
1942
|
+
return;
|
|
1943
|
+
rb_set_errinfo(Qnil);
|
|
1944
|
+
RejectionList batch;
|
|
1945
|
+
rejection_list_split_oldest(data->context, &data->pending_rejections, n, &batch);
|
|
1946
|
+
notify_rejection_batch(data, &batch);
|
|
1947
|
+
rejection_list_free(data->context, &batch);
|
|
1948
|
+
rb_set_errinfo(r_errinfo);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// Reports every pending rejection, then once more for what the handler
|
|
1952
|
+
// rejected meanwhile, and no further. The second round is left to whoever
|
|
1953
|
+
// drains the queue if the first one queued jobs that may still run.
|
|
1954
|
+
static void quickjsrb_notify_all_rejections(VMData *data, bool jobs_may_run)
|
|
1955
|
+
{
|
|
1956
|
+
quickjsrb_notify_oldest_rejections(data, data->pending_rejections.live);
|
|
1957
|
+
if (jobs_may_run && JS_IsJobPending(JS_GetRuntime(data->context)))
|
|
1958
|
+
return;
|
|
1959
|
+
quickjsrb_notify_oldest_rejections(data, data->pending_rejections.live);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// With jobs still queued, whoever drains them ends the checkpoint; until then
|
|
1963
|
+
// at most max_pending_rejections are carried over, the oldest reported first.
|
|
1964
|
+
static void quickjsrb_end_microtask_checkpoint(VMData *data)
|
|
1965
|
+
{
|
|
1966
|
+
if (data->oom_poisoned)
|
|
1967
|
+
return;
|
|
1968
|
+
uint32_t live = data->pending_rejections.live;
|
|
1969
|
+
if (!JS_IsJobPending(JS_GetRuntime(data->context)))
|
|
1970
|
+
quickjsrb_notify_all_rejections(data, true);
|
|
1971
|
+
else if (live > data->max_pending_rejections)
|
|
1972
|
+
quickjsrb_notify_oldest_rejections(data, live - data->max_pending_rejections);
|
|
1973
|
+
// Give back what an emptied list still holds, rather than keep its peak.
|
|
1974
|
+
if (data->pending_rejections.live == 0)
|
|
1975
|
+
rejection_list_free(data->context, &data->pending_rejections);
|
|
753
1976
|
}
|
|
754
1977
|
|
|
755
1978
|
static VALUE r_try_call_proc(VALUE r_try_args)
|
|
@@ -764,6 +1987,25 @@ static VALUE r_try_call_proc(VALUE r_try_args)
|
|
|
764
1987
|
);
|
|
765
1988
|
}
|
|
766
1989
|
|
|
1990
|
+
// The argument conversion, run under rb_protect by the bridge below. The
|
|
1991
|
+
// values are borrowed: QuickJS owns argv for the length of the call, and
|
|
1992
|
+
// to_rb_value does not consume what it is given.
|
|
1993
|
+
struct call_args_conv
|
|
1994
|
+
{
|
|
1995
|
+
JSContext *ctx;
|
|
1996
|
+
int argc;
|
|
1997
|
+
JSValueConst *argv;
|
|
1998
|
+
VALUE r_argv;
|
|
1999
|
+
};
|
|
2000
|
+
|
|
2001
|
+
static VALUE r_convert_call_args(VALUE r_work)
|
|
2002
|
+
{
|
|
2003
|
+
struct call_args_conv *work = (struct call_args_conv *)r_work;
|
|
2004
|
+
for (int i = 0; i < work->argc; i++)
|
|
2005
|
+
rb_ary_push(work->r_argv, to_rb_value(work->ctx, work->argv[i]));
|
|
2006
|
+
return Qnil;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
767
2009
|
static JSValue js_quickjsrb_call_global(JSContext *ctx, JSValueConst _this, int argc, JSValueConst *argv, int _magic, JSValue *func_data)
|
|
768
2010
|
{
|
|
769
2011
|
// func_data[0] holds the Ruby Symbol ID for the defined function (stored by
|
|
@@ -774,21 +2016,110 @@ static JSValue js_quickjsrb_call_global(JSContext *ctx, JSValueConst _this, int
|
|
|
774
2016
|
|
|
775
2017
|
VMData *data = JS_GetContextOpaque(ctx);
|
|
776
2018
|
VALUE r_proc = rb_hash_aref(data->defined_functions, ID2SYM((ID)key_id));
|
|
777
|
-
//
|
|
2019
|
+
// Entries are never removed, and the block is recorded once the property is
|
|
2020
|
+
// installed, so the one way to get here is from inside that install: a
|
|
2021
|
+
// setter on the target that calls the value it was handed. Say so, rather
|
|
2022
|
+
// than report an internal fault to a caller whose setter did exactly that.
|
|
778
2023
|
if (r_proc == Qnil)
|
|
779
2024
|
{
|
|
780
|
-
return JS_ThrowReferenceError(ctx, "
|
|
2025
|
+
return JS_ThrowReferenceError(ctx, "'%s' cannot be called yet: define_function has not finished installing it", rb_id2name((ID)key_id));
|
|
781
2026
|
}
|
|
782
2027
|
|
|
783
2028
|
VALUE r_call_args = rb_ary_new();
|
|
784
2029
|
rb_ary_push(r_call_args, r_proc);
|
|
785
2030
|
|
|
2031
|
+
// Protected, like the call below it. Converting an argument runs guest code
|
|
2032
|
+
// and can raise, and this is inside a JSCFunction: an unprotected raise
|
|
2033
|
+
// unwinds through QuickJS, taking the values this frame holds with it, and
|
|
2034
|
+
// the guest cannot catch what every other error from this bridge is
|
|
2035
|
+
// catchable. Measured before this, a revoked proxy argument cost 7 QuickJS
|
|
2036
|
+
// objects and about 1.5KB per call, at a rate the guest picks.
|
|
786
2037
|
VALUE r_argv = rb_ary_new();
|
|
787
|
-
|
|
2038
|
+
struct call_args_conv conv_work = {ctx, argc, argv, r_argv};
|
|
2039
|
+
int conv_failed;
|
|
2040
|
+
rb_protect(r_convert_call_args, (VALUE)&conv_work, &conv_failed);
|
|
2041
|
+
if (conv_failed)
|
|
788
2042
|
{
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
2043
|
+
VALUE r_conv_error = rb_errinfo();
|
|
2044
|
+
rb_set_errinfo(Qnil);
|
|
2045
|
+
|
|
2046
|
+
// Two things outrank whatever else the conversion raised, and neither is
|
|
2047
|
+
// the guest's to catch. rb_protect catches everything, so without this the
|
|
2048
|
+
// deadline came back as an ordinary Error and a script that swallowed it
|
|
2049
|
+
// returned a value for a run that had already overrun; and an
|
|
2050
|
+
// out-of-memory came back the same way, on a heap whose own latch says
|
|
2051
|
+
// further evaluation may segfault, leaving the guest free to keep
|
|
2052
|
+
// allocating until the next Ruby entry point refuses. The same two calls
|
|
2053
|
+
// js_poll_interrupts makes, which is what the log bridge does with its own
|
|
2054
|
+
// protect for the same reason.
|
|
2055
|
+
if (eval_budget_lapsed_now(ctx))
|
|
2056
|
+
{
|
|
2057
|
+
JS_ThrowInternalError(ctx, "interrupted");
|
|
2058
|
+
JS_SetUncatchableException(ctx, TRUE);
|
|
2059
|
+
return JS_EXCEPTION;
|
|
2060
|
+
}
|
|
2061
|
+
if (data->oom_poisoned)
|
|
2062
|
+
{
|
|
2063
|
+
// Uncatchable for the same reason, and said in its own words: reporting
|
|
2064
|
+
// this as an interruption would tell the caller the run ran out of time
|
|
2065
|
+
// when it ran out of memory, and the top-level renderer classifies by
|
|
2066
|
+
// the message.
|
|
2067
|
+
JS_ThrowInternalError(ctx, "out of memory");
|
|
2068
|
+
JS_SetUncatchableException(ctx, TRUE);
|
|
2069
|
+
return JS_EXCEPTION;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// Only an Exception can be bridged: rb_protect reports every non-local
|
|
2073
|
+
// exit, and a Ruby throw leaves data in errinfo that j_error_from_ruby_error
|
|
2074
|
+
// would immediately send #message to. The promise rejection tracker guards
|
|
2075
|
+
// the same way.
|
|
2076
|
+
if (!rb_obj_is_kind_of(r_conv_error, rb_eException))
|
|
2077
|
+
return JS_ThrowInternalError(ctx, "quickjs: converting an argument was interrupted by a non-local exit");
|
|
2078
|
+
|
|
2079
|
+
// The capability is taken before the error is built, not after: building
|
|
2080
|
+
// the error publishes a handle into alive_objects, and a capability that
|
|
2081
|
+
// then failed to allocate would leave that entry with nothing able to read
|
|
2082
|
+
// it back out, pinning the Ruby exception for the life of the VM. The
|
|
2083
|
+
// async path below this function is ordered the same way for the same
|
|
2084
|
+
// reason.
|
|
2085
|
+
bool is_async = JS_ToBool(ctx, func_data[1]);
|
|
2086
|
+
JSValue resolving_funcs[2];
|
|
2087
|
+
JSValue promise = JS_UNDEFINED;
|
|
2088
|
+
if (is_async)
|
|
2089
|
+
{
|
|
2090
|
+
promise = JS_NewPromiseCapability(ctx, resolving_funcs);
|
|
2091
|
+
if (JS_IsException(promise))
|
|
2092
|
+
return JS_EXCEPTION;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// Handed back the way the call's own failures are: as a JS error carrying
|
|
2096
|
+
// the host exception, so a guest that wrapped this in try/catch sees it
|
|
2097
|
+
// and the host still gets it back if the throw travels out. An async
|
|
2098
|
+
// function rejects with it rather than throwing at the call, since that is
|
|
2099
|
+
// the shape its caller was written against.
|
|
2100
|
+
JSValue j_conv_error = j_error_from_ruby_error(ctx, r_conv_error);
|
|
2101
|
+
if (JS_IsException(j_conv_error))
|
|
2102
|
+
{
|
|
2103
|
+
if (is_async)
|
|
2104
|
+
{
|
|
2105
|
+
JS_FreeValue(ctx, promise);
|
|
2106
|
+
JS_FreeValue(ctx, resolving_funcs[0]);
|
|
2107
|
+
JS_FreeValue(ctx, resolving_funcs[1]);
|
|
2108
|
+
}
|
|
2109
|
+
return JS_EXCEPTION;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
if (is_async)
|
|
2113
|
+
{
|
|
2114
|
+
JSValue j_settled = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1, (JSValueConst *)&j_conv_error);
|
|
2115
|
+
JS_FreeValue(ctx, j_conv_error);
|
|
2116
|
+
JS_FreeValue(ctx, j_settled);
|
|
2117
|
+
JS_FreeValue(ctx, resolving_funcs[0]);
|
|
2118
|
+
JS_FreeValue(ctx, resolving_funcs[1]);
|
|
2119
|
+
return promise;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
return JS_Throw(ctx, j_conv_error);
|
|
792
2123
|
}
|
|
793
2124
|
rb_ary_push(r_call_args, r_argv);
|
|
794
2125
|
rb_ary_push(r_call_args, ULONG2NUM(data->eval_time->limit_ms));
|
|
@@ -810,13 +2141,42 @@ static JSValue js_quickjsrb_call_global(JSContext *ctx, JSValueConst _this, int
|
|
|
810
2141
|
if (sadnessHappened)
|
|
811
2142
|
{
|
|
812
2143
|
VALUE r_error = rb_errinfo();
|
|
2144
|
+
// Cleared, like every other recovery site here. Left set, it survives
|
|
2145
|
+
// eval_code returning normally: a guest that merely catches the throw
|
|
2146
|
+
// leaves the host's $! holding it, later raises chain to it through
|
|
2147
|
+
// cause (#126), and a script that does nothing else exits 1 with a
|
|
2148
|
+
// printed backtrace it never raised.
|
|
2149
|
+
rb_set_errinfo(Qnil);
|
|
813
2150
|
j_result = j_error_from_ruby_error(ctx, r_error);
|
|
2151
|
+
// Rejecting with the sentinel would hand the guest a value it cannot
|
|
2152
|
+
// name, so the promise is left unsettled and the throw that produced it,
|
|
2153
|
+
// already pending, is what the caller gets. Freed on the way out with
|
|
2154
|
+
// everything else this block holds: returning from here directly would
|
|
2155
|
+
// leak the capability and keep its resolving functions alive for the
|
|
2156
|
+
// life of the runtime.
|
|
2157
|
+
if (JS_IsException(j_result))
|
|
2158
|
+
{
|
|
2159
|
+
JS_FreeValue(ctx, promise);
|
|
2160
|
+
JS_FreeValue(ctx, resolving_funcs[0]);
|
|
2161
|
+
JS_FreeValue(ctx, resolving_funcs[1]);
|
|
2162
|
+
return JS_EXCEPTION;
|
|
2163
|
+
}
|
|
814
2164
|
ret_val = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED,
|
|
815
2165
|
1, (JSValueConst *)&j_result);
|
|
816
2166
|
}
|
|
817
2167
|
else
|
|
818
2168
|
{
|
|
819
2169
|
j_result = to_js_value(ctx, r_result);
|
|
2170
|
+
// Resolving with the sentinel would put it inside the promise; the throw
|
|
2171
|
+
// that produced it is pending for the caller instead.
|
|
2172
|
+
if (JS_IsException(j_result))
|
|
2173
|
+
{
|
|
2174
|
+
JS_FreeValue(ctx, promise);
|
|
2175
|
+
JS_FreeValue(ctx, resolving_funcs[0]);
|
|
2176
|
+
JS_FreeValue(ctx, resolving_funcs[1]);
|
|
2177
|
+
return JS_EXCEPTION;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
820
2180
|
ret_val = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED,
|
|
821
2181
|
1, (JSValueConst *)&j_result);
|
|
822
2182
|
}
|
|
@@ -832,7 +2192,10 @@ static JSValue js_quickjsrb_call_global(JSContext *ctx, JSValueConst _this, int
|
|
|
832
2192
|
if (sadnessHappened)
|
|
833
2193
|
{
|
|
834
2194
|
VALUE r_error = rb_errinfo();
|
|
2195
|
+
rb_set_errinfo(Qnil);
|
|
835
2196
|
JSValue j_error = j_error_from_ruby_error(ctx, r_error);
|
|
2197
|
+
if (JS_IsException(j_error))
|
|
2198
|
+
return JS_EXCEPTION;
|
|
836
2199
|
return JS_Throw(ctx, j_error);
|
|
837
2200
|
}
|
|
838
2201
|
else
|
|
@@ -914,19 +2277,23 @@ struct quickjsrb_log_call
|
|
|
914
2277
|
JSValueConst *argv;
|
|
915
2278
|
const char *severity;
|
|
916
2279
|
JSValue result;
|
|
2280
|
+
// Set when the budget lapsed inside one of the logged values; read back on
|
|
2281
|
+
// the QuickJS side of the protect, where it can be thrown as the interrupt.
|
|
2282
|
+
bool interrupted;
|
|
917
2283
|
};
|
|
918
2284
|
|
|
919
|
-
|
|
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)
|
|
2285
|
+
struct log_row_build
|
|
926
2286
|
{
|
|
927
|
-
struct quickjsrb_log_call *call
|
|
2287
|
+
struct quickjsrb_log_call *call;
|
|
2288
|
+
JsHold hold;
|
|
2289
|
+
};
|
|
2290
|
+
|
|
2291
|
+
static VALUE r_build_log_row(VALUE r_build)
|
|
2292
|
+
{
|
|
2293
|
+
struct log_row_build *build = (struct log_row_build *)r_build;
|
|
2294
|
+
struct quickjsrb_log_call *call = build->call;
|
|
928
2295
|
JSContext *ctx = call->ctx;
|
|
929
|
-
|
|
2296
|
+
JsHold *hold = &build->hold;
|
|
930
2297
|
VALUE r_row = rb_ary_new();
|
|
931
2298
|
for (int i = 0; i < call->argc; i++)
|
|
932
2299
|
{
|
|
@@ -938,40 +2305,57 @@ static VALUE r_build_and_dispatch_log(VALUE r_call)
|
|
|
938
2305
|
}
|
|
939
2306
|
else if (JS_IsError(ctx, j_logged))
|
|
940
2307
|
{
|
|
941
|
-
JSValue j_errorClassName = JS_GetPropertyStr(ctx, j_logged, "name");
|
|
942
|
-
const char *errorClassName =
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
JS_FreeValue(ctx, j_stackTrace);
|
|
952
|
-
|
|
953
|
-
const char *headlineTemplate = "%s: %s\n%s";
|
|
954
|
-
int length = snprintf(NULL, 0, headlineTemplate, errorClassName, errorClassMessage, stackTrace);
|
|
955
|
-
char *headline = (char *)malloc(length + 1);
|
|
956
|
-
snprintf(headline, length + 1, headlineTemplate, errorClassName, errorClassMessage, stackTrace);
|
|
957
|
-
JS_FreeCString(ctx, errorClassName);
|
|
958
|
-
JS_FreeCString(ctx, errorClassMessage);
|
|
959
|
-
JS_FreeCString(ctx, stackTrace);
|
|
960
|
-
|
|
961
|
-
r_raw = rb_str_new2(headline);
|
|
962
|
-
free(headline);
|
|
2308
|
+
JSValue j_errorClassName = js_hold_value(hold, JS_GetPropertyStr(ctx, j_logged, "name"));
|
|
2309
|
+
const char *errorClassName = js_hold_cstring(hold, j_errorClassName, "Error");
|
|
2310
|
+
|
|
2311
|
+
JSValue j_errorClassMessage = js_hold_value(hold, JS_GetPropertyStr(ctx, j_logged, "message"));
|
|
2312
|
+
const char *errorClassMessage = js_hold_cstring(hold, j_errorClassMessage, QUICKJSRB_UNRENDERABLE);
|
|
2313
|
+
|
|
2314
|
+
JSValue j_stackTrace = js_hold_value(hold, JS_GetPropertyStr(ctx, j_logged, "stack"));
|
|
2315
|
+
const char *stackTrace = js_hold_cstring(hold, j_stackTrace, "");
|
|
2316
|
+
|
|
2317
|
+
r_raw = rb_str_new2(js_hold_format(hold, "%s: %s\n%s", errorClassName, errorClassMessage, stackTrace));
|
|
963
2318
|
}
|
|
964
2319
|
else
|
|
965
2320
|
{
|
|
966
|
-
r_raw =
|
|
2321
|
+
r_raw = to_rb_value_substituting(ctx, j_logged);
|
|
967
2322
|
}
|
|
968
|
-
|
|
969
|
-
VALUE r_c = rb_str_new2(body);
|
|
970
|
-
JS_FreeCString(ctx, body);
|
|
2323
|
+
VALUE r_c = rb_str_new2(js_hold_cstring(hold, j_logged, QUICKJSRB_UNRENDERABLE));
|
|
971
2324
|
|
|
972
2325
|
rb_ary_push(r_row, r_log_body_new(r_raw, r_c));
|
|
2326
|
+
|
|
2327
|
+
// Each argument's references go back as its entry is finished, the way the
|
|
2328
|
+
// block released them one at a time before; the ensure covers a raise.
|
|
2329
|
+
js_hold_release((VALUE)hold);
|
|
973
2330
|
}
|
|
974
2331
|
|
|
2332
|
+
return r_row;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// Runs under rb_protect (see js_quickjsrb_log_inner) so no Ruby raise —
|
|
2336
|
+
// to_rb_value on an unconvertible argument (e.g. a Promise nested inside an
|
|
2337
|
+
// array), a bridge reporting through a logged value's toString, allocation
|
|
2338
|
+
// failure, or the user's on_log listener — can longjmp through QuickJS's
|
|
2339
|
+
// interpreter frames, or, on the pure path (where this runs inside
|
|
2340
|
+
// rb_thread_call_with_gvl), across the rb_thread_call_without_gvl region —
|
|
2341
|
+
// which would leak its buffers and leave gvl_released_js stuck.
|
|
2342
|
+
static VALUE r_build_and_dispatch_log(VALUE r_call)
|
|
2343
|
+
{
|
|
2344
|
+
struct quickjsrb_log_call *call = (struct quickjsrb_log_call *)r_call;
|
|
2345
|
+
VMData *data = JS_GetContextOpaque(call->ctx);
|
|
2346
|
+
|
|
2347
|
+
struct log_row_build build;
|
|
2348
|
+
build.call = call;
|
|
2349
|
+
js_hold_init(&build.hold, call->ctx);
|
|
2350
|
+
VALUE r_row = rb_ensure(r_build_log_row, (VALUE)&build, js_hold_release, (VALUE)&build.hold);
|
|
2351
|
+
|
|
2352
|
+
// Carried out rather than raised here: a Ruby exception would be bridged
|
|
2353
|
+
// into a catchable JS Error, and a guest wrapping console.log in try/catch
|
|
2354
|
+
// could swallow the timeout and pin one InterruptedError in alive_objects
|
|
2355
|
+
// per catch. The caller throws it as the interrupt QuickJS itself would.
|
|
2356
|
+
// Copied before the listener runs, since a listener that raises unwinds
|
|
2357
|
+
// past everything after it.
|
|
2358
|
+
call->interrupted = build.hold.interrupted;
|
|
975
2359
|
r_call_log_listener(rb_ary_new3(2, data->log_listener, r_log_new(call->severity, r_row)));
|
|
976
2360
|
return Qnil;
|
|
977
2361
|
}
|
|
@@ -981,14 +2365,30 @@ static VALUE r_build_and_dispatch_log(VALUE r_call)
|
|
|
981
2365
|
// JS exception instead of a cross-boundary longjmp.
|
|
982
2366
|
static JSValue js_quickjsrb_log_inner(JSContext *ctx, int argc, JSValueConst *argv, const char *severity)
|
|
983
2367
|
{
|
|
984
|
-
struct quickjsrb_log_call call = {ctx, argc, argv, severity, JS_UNDEFINED};
|
|
2368
|
+
struct quickjsrb_log_call call = {ctx, argc, argv, severity, JS_UNDEFINED, false};
|
|
985
2369
|
int error;
|
|
986
2370
|
rb_protect(r_build_and_dispatch_log, (VALUE)&call, &error);
|
|
2371
|
+
if (call.interrupted)
|
|
2372
|
+
{
|
|
2373
|
+
// The lapse outranks a raise from the listener, as it does in the uncaught
|
|
2374
|
+
// renderer, where dispatch_log swallows the listener's raise on its own:
|
|
2375
|
+
// the budget is gone, and reporting the listener instead would hand the
|
|
2376
|
+
// guest a catchable error to repeat the overrun behind.
|
|
2377
|
+
if (error)
|
|
2378
|
+
rb_set_errinfo(Qnil);
|
|
2379
|
+
// The same two calls js_poll_interrupts makes, so the guest cannot catch it
|
|
2380
|
+
// and the top-level renderer classifies it exactly as a native timeout.
|
|
2381
|
+
JS_ThrowInternalError(ctx, "interrupted");
|
|
2382
|
+
JS_SetUncatchableException(ctx, TRUE);
|
|
2383
|
+
return JS_EXCEPTION;
|
|
2384
|
+
}
|
|
987
2385
|
if (error)
|
|
988
2386
|
{
|
|
989
2387
|
VALUE r_error = rb_errinfo();
|
|
990
2388
|
rb_set_errinfo(Qnil);
|
|
991
2389
|
JSValue j_error = j_error_from_ruby_error(ctx, r_error);
|
|
2390
|
+
if (JS_IsException(j_error))
|
|
2391
|
+
return JS_EXCEPTION;
|
|
992
2392
|
return JS_Throw(ctx, j_error);
|
|
993
2393
|
}
|
|
994
2394
|
return JS_UNDEFINED;
|
|
@@ -1060,35 +2460,49 @@ static JSValue js_console_error(JSContext *ctx, JSValueConst this, int argc, JSV
|
|
|
1060
2460
|
return js_quickjsrb_log(ctx, argc, argv, "error");
|
|
1061
2461
|
}
|
|
1062
2462
|
|
|
1063
|
-
// Run
|
|
1064
|
-
//
|
|
1065
|
-
// on multi-core hosts.
|
|
2463
|
+
// Run bytecode load + eval without the GVL so background threads (warmer
|
|
2464
|
+
// pools populating VMs, per-thread Runnable#run) proceed in parallel with
|
|
2465
|
+
// the main thread on multi-core hosts.
|
|
1066
2466
|
//
|
|
1067
|
-
//
|
|
2467
|
+
// Four call sites use the run_bytecode_release_gvl wrapper below:
|
|
1068
2468
|
//
|
|
1069
|
-
// 1. vm_m_initialize — pre-built bytecode (file / encoding /
|
|
1070
|
-
// as static C constants. setTimeout (FEATURE_TIMEOUT)
|
|
1071
|
-
// proxy (POLYFILL_FILE, registered ahead of the
|
|
1072
|
-
// can already be live here, so the release is
|
|
1073
|
-
// nothing is registered, but because a load never
|
|
1074
|
-
// js_quickjsrb_set_timeout only enqueues (pure C;
|
|
1075
|
-
// js_delay_and_eval_job runs later, under a
|
|
1076
|
-
// load never drains the job queue, and the
|
|
1077
|
-
// top-levels (built from polyfills/src in this
|
|
1078
|
-
// File proxy. That audit is the invariant to
|
|
1079
|
-
// bundles or reordering vm_m_initialize.
|
|
2469
|
+
// 1. vm_m_initialize — pre-built polyfill bytecode (file / encoding /
|
|
2470
|
+
// url) embedded as static C constants. setTimeout (FEATURE_TIMEOUT)
|
|
2471
|
+
// and the File proxy (POLYFILL_FILE, registered ahead of the
|
|
2472
|
+
// encoding/url loads) can already be live here, so the release is
|
|
2473
|
+
// safe not because nothing is registered, but because a load never
|
|
2474
|
+
// runs bridge code: js_quickjsrb_set_timeout only enqueues (pure C;
|
|
2475
|
+
// the Ruby-calling js_delay_and_eval_job runs later, under a
|
|
2476
|
+
// GVL-held drain/await), a load never drains the job queue, and the
|
|
2477
|
+
// bundled polyfill top-levels (built from polyfills/src in this
|
|
2478
|
+
// repo) don't call the File proxy. That audit is the invariant to
|
|
2479
|
+
// preserve when rebuilding bundles or reordering vm_m_initialize.
|
|
1080
2480
|
//
|
|
1081
|
-
// 2. vm_m_loadPolyfillBytecode — bytecode from a Ruby String, copied to
|
|
1082
|
-
// malloc'd buffer first so the buffer survives a GC compact. This
|
|
2481
|
+
// 2. vm_m_loadPolyfillBytecode — bytecode from a Ruby String, copied to
|
|
2482
|
+
// a malloc'd buffer first so the buffer survives a GC compact. This
|
|
1083
2483
|
// bytecode is arbitrary (registered by companion gems), so no audit
|
|
1084
2484
|
// applies: the caller gates the release on can_eval_gvl_free() to
|
|
1085
2485
|
// bail whenever a direct-rb_funcall bridge (File proxy, crypto, …)
|
|
1086
2486
|
// is installed; console.log is covered by js_quickjsrb_log's
|
|
1087
2487
|
// gvl_released_js re-acquire either way.
|
|
1088
2488
|
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
// the
|
|
2489
|
+
// 3. vm_m_evalBytecode — user bytecode via Runnable#run, same gate and
|
|
2490
|
+
// buffer copy as 2, but with the awaiting runner: js_std_await's job
|
|
2491
|
+
// drain is bridge-free under the gate, the same argument eval_code's
|
|
2492
|
+
// released path relies on.
|
|
2493
|
+
//
|
|
2494
|
+
// 4. vm_m_preloadModuleBytecode — module bytecode from a Ruby String,
|
|
2495
|
+
// copied like 2/3, but with the deserialize-only runner
|
|
2496
|
+
// (bytecode_read_job_run) and no can_eval_gvl_free gate. A preload
|
|
2497
|
+
// registers the module def without running any top level, so no bridge
|
|
2498
|
+
// can fire regardless of what's installed — the release is
|
|
2499
|
+
// unconditional, and safe even on a bridged VM where 2/3 fall back to
|
|
2500
|
+
// the GVL-held path.
|
|
2501
|
+
//
|
|
2502
|
+
// run_bytecode_release_gvl delegates to the shared GVL-release region
|
|
2503
|
+
// (see run_gvl_release_region) so every caller inherits the re-acquire
|
|
2504
|
+
// safety, the dispose! handshake, and interrupt-proof cleanup
|
|
2505
|
+
// automatically.
|
|
1092
2506
|
struct bytecode_load_job
|
|
1093
2507
|
{
|
|
1094
2508
|
JSContext *ctx;
|
|
@@ -1098,8 +2512,8 @@ struct bytecode_load_job
|
|
|
1098
2512
|
};
|
|
1099
2513
|
|
|
1100
2514
|
// Shared bytecode read + eval core — pure C over JSValues, MUST NOT touch
|
|
1101
|
-
// the Ruby VM (the polyfill release
|
|
1102
|
-
// GVL-held call sites invoke it directly).
|
|
2515
|
+
// the Ruby VM (the polyfill and bytecode-run release paths run it without
|
|
2516
|
+
// the GVL; the GVL-held call sites invoke it directly).
|
|
1103
2517
|
static void *bytecode_load_job_run(void *p)
|
|
1104
2518
|
{
|
|
1105
2519
|
struct bytecode_load_job *job = p;
|
|
@@ -1116,16 +2530,106 @@ static void *bytecode_load_job_run(void *p)
|
|
|
1116
2530
|
return NULL;
|
|
1117
2531
|
}
|
|
1118
2532
|
|
|
1119
|
-
//
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
1122
|
-
|
|
2533
|
+
// Awaiting variant of the core, for vm_m_evalBytecode: js_std_await drains
|
|
2534
|
+
// the job queue until the eval's promise settles. Same MUST-NOT-touch-Ruby
|
|
2535
|
+
// constraint — on a pure VM (can_eval_gvl_free) every drained job is
|
|
2536
|
+
// bridge-free JS, the same argument eval_code's released path relies on.
|
|
2537
|
+
// js_std_await passes a non-promise — including an exception preserved by
|
|
2538
|
+
// the JS_ReadObject short-circuit — through untouched.
|
|
2539
|
+
static void *bytecode_eval_await_job_run(void *p)
|
|
2540
|
+
{
|
|
2541
|
+
struct bytecode_load_job *job = p;
|
|
2542
|
+
bytecode_load_job_run(job);
|
|
2543
|
+
job->result = quickjsrb_host_await(job->ctx, job->result);
|
|
2544
|
+
return NULL;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// Deserialize-only variant, for vm_m_preloadModuleBytecode: reads a module
|
|
2548
|
+
// def into ctx->loaded_modules without evaluating it, so a later import can
|
|
2549
|
+
// link and run it. Unlike the two runners above this never reaches JS
|
|
2550
|
+
// execution or the job queue — JS_ReadObject only deserializes, deferring
|
|
2551
|
+
// import resolution to js_link_module at eval time — so its release needs no
|
|
2552
|
+
// can_eval_gvl_free gate: there is no top level that could reach a Ruby
|
|
2553
|
+
// bridge, on any VM. Same MUST-NOT-touch-Ruby constraint all the same.
|
|
2554
|
+
static void *bytecode_read_job_run(void *p)
|
|
2555
|
+
{
|
|
2556
|
+
struct bytecode_load_job *job = p;
|
|
2557
|
+
job->result = JS_ReadObject(job->ctx, job->buf, job->buf_len, JS_READ_OBJ_BYTECODE);
|
|
2558
|
+
return NULL;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// job_run: bytecode_load_job_run (load only) or bytecode_eval_await_job_run
|
|
2562
|
+
// (load + await). take_ownership: true when buf is malloc'd storage that
|
|
2563
|
+
// the release region should free on every exit path (including
|
|
2564
|
+
// async-interrupt unwinds); false when buf points at static bytecode.
|
|
2565
|
+
static JSValue run_bytecode_release_gvl(VMData *data, void *(*job_run)(void *), const uint8_t *buf, size_t buf_len, bool take_ownership)
|
|
1123
2566
|
{
|
|
1124
2567
|
struct bytecode_load_job job = {data->context, buf, buf_len, JS_UNDEFINED};
|
|
1125
|
-
run_gvl_release_region(data,
|
|
2568
|
+
run_gvl_release_region(data, job_run, &job, &job.result, take_ownership ? (uint8_t *)buf : NULL, NULL);
|
|
1126
2569
|
return job.result;
|
|
1127
2570
|
}
|
|
1128
2571
|
|
|
2572
|
+
// Every polyfill load takes the load-only runner; naming that keeps the
|
|
2573
|
+
// call sites, which pass their result straight to finish_polyfill_load,
|
|
2574
|
+
// down to one line.
|
|
2575
|
+
static JSValue load_polyfill_bytecode(VMData *data, const uint8_t *buf, size_t buf_len, bool take_ownership)
|
|
2576
|
+
{
|
|
2577
|
+
return run_bytecode_release_gvl(data, bytecode_load_job_run, buf, buf_len, take_ownership);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// Shared settle check for every polyfill load's result, bundled or
|
|
2581
|
+
// registered. The compiled bytecode is async-wrapped (JS_EVAL_FLAG_ASYNC),
|
|
2582
|
+
// so a top-level throw comes back as a rejected promise, not JS_EXCEPTION;
|
|
2583
|
+
// re-throwing the reason routes it through to_rb_value's standard
|
|
2584
|
+
// exception path (interrupted/OOM mapping, oom_poisoned latching, on_log
|
|
2585
|
+
// dispatch). And a top level that awaits is still pending: loads never
|
|
2586
|
+
// drain the job queue, so nothing past the first await has run — refuse
|
|
2587
|
+
// loudly instead of shipping a silently half-applied VM; polyfill top
|
|
2588
|
+
// levels must settle synchronously (the register_polyfill contract).
|
|
2589
|
+
// That refusal reuses NoAwaitError, the same class eval_code raises for
|
|
2590
|
+
// a promise left unawaited at the top level.
|
|
2591
|
+
// Frees j_result on every path.
|
|
2592
|
+
static void finish_polyfill_load(VMData *data, JSValue j_result)
|
|
2593
|
+
{
|
|
2594
|
+
if (JS_IsException(j_result))
|
|
2595
|
+
{
|
|
2596
|
+
to_rb_value(data->context, j_result); // raises
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
JSPromiseStateEnum state = JS_PromiseState(data->context, j_result);
|
|
2601
|
+
if (state == JS_PROMISE_REJECTED)
|
|
2602
|
+
{
|
|
2603
|
+
JSValue j_reason = JS_PromiseResult(data->context, j_result);
|
|
2604
|
+
JS_FreeValue(data->context, j_result);
|
|
2605
|
+
JS_Throw(data->context, j_reason); // consumes j_reason
|
|
2606
|
+
to_rb_value(data->context, JS_EXCEPTION); // raises
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
if (state == JS_PROMISE_PENDING)
|
|
2610
|
+
{
|
|
2611
|
+
JS_FreeValue(data->context, j_result);
|
|
2612
|
+
VALUE r_msg = rb_str_new2("polyfill top level must settle synchronously: top-level await leaves the load pending and the polyfill silently half-applied");
|
|
2613
|
+
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_NO_AWAIT_ERROR), rb_intern("new"), 2, r_msg, Qnil));
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
JS_FreeValue(data->context, j_result);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// Both size options are read straight into a size_t, where a negative turns
|
|
2620
|
+
// into SIZE_MAX rather than an error: for max_stack_size that puts
|
|
2621
|
+
// stack_limit above stack_top so every eval raises "stack overflow", and for
|
|
2622
|
+
// memory_limit it reads as an enormous budget. Neither is what the caller
|
|
2623
|
+
// typed, so both are refused here, by name, while the name still means
|
|
2624
|
+
// something to them.
|
|
2625
|
+
static size_t size_option(VALUE r_value, const char *name)
|
|
2626
|
+
{
|
|
2627
|
+
if (!RB_INTEGER_TYPE_P(r_value) || RTEST(rb_funcall(r_value, rb_intern("negative?"), 0)))
|
|
2628
|
+
rb_raise(rb_eArgError, "%s must be a non-negative Integer", name);
|
|
2629
|
+
|
|
2630
|
+
return NUM2SIZET(r_value);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
1129
2633
|
static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
1130
2634
|
{
|
|
1131
2635
|
VALUE r_opts;
|
|
@@ -1145,20 +2649,51 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
1145
2649
|
VALUE r_timeout_msec = rb_hash_aref(r_opts, ID2SYM(rb_intern("timeout_msec")));
|
|
1146
2650
|
if (NIL_P(r_timeout_msec))
|
|
1147
2651
|
r_timeout_msec = UINT2NUM(100);
|
|
2652
|
+
VALUE r_max_pending_rejections = rb_hash_aref(r_opts, ID2SYM(rb_intern("max_pending_rejections")));
|
|
2653
|
+
size_t max_pending_rejections =
|
|
2654
|
+
NIL_P(r_max_pending_rejections) ? 1000 : size_option(r_max_pending_rejections, "max_pending_rejections");
|
|
1148
2655
|
|
|
1149
2656
|
VMData *data;
|
|
1150
2657
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1151
2658
|
|
|
2659
|
+
// Reachable only through send, since initialize is private, but reachable:
|
|
2660
|
+
// everything below here writes to the runtime, so this is a JS entry point
|
|
2661
|
+
// in all but name and was the one place not saying so. Disposed first,
|
|
2662
|
+
// because JS_SetContextOpaque on the next line dereferences a context that
|
|
2663
|
+
// dispose! has already freed, which segfaults rather than raising. Then the
|
|
2664
|
+
// owner, because the same writes landing beside another thread's in-flight
|
|
2665
|
+
// JS_Eval is the corruption this whole file is arranged to refuse.
|
|
2666
|
+
check_disposed(data);
|
|
2667
|
+
check_js_entry_owner(data);
|
|
2668
|
+
|
|
1152
2669
|
data->eval_time->limit_ms = (int64_t)NUM2UINT(r_timeout_msec);
|
|
2670
|
+
data->max_pending_rejections = max_pending_rejections > UINT32_MAX ? UINT32_MAX : (uint32_t)max_pending_rejections;
|
|
1153
2671
|
JS_SetContextOpaque(data->context, data);
|
|
2672
|
+
// Learned here, before the first line of guest code: the constructor is read
|
|
2673
|
+
// off the global, and a script that reassigns or deletes Proxy would
|
|
2674
|
+
// otherwise choose what every reader below treats as one.
|
|
2675
|
+
// Once per VM. initialize is private but reachable through send, and by then
|
|
2676
|
+
// the guest may have replaced the constructor this reads.
|
|
2677
|
+
if (data->proxy_class_id == 0)
|
|
2678
|
+
{
|
|
2679
|
+
static const char probe[] = "new Proxy({}, {})";
|
|
2680
|
+
JSValue j_probe = JS_Eval(data->context, probe, sizeof(probe) - 1, "<probe>", JS_EVAL_TYPE_GLOBAL);
|
|
2681
|
+
if (JS_IsException(j_probe))
|
|
2682
|
+
JS_FreeValue(data->context, JS_GetException(data->context));
|
|
2683
|
+
else
|
|
2684
|
+
data->proxy_class_id = JS_GetClassID(j_probe);
|
|
2685
|
+
JS_FreeValue(data->context, j_probe);
|
|
2686
|
+
}
|
|
1154
2687
|
JSRuntime *runtime = JS_GetRuntime(data->context);
|
|
1155
2688
|
|
|
1156
|
-
JS_SetMemoryLimit(runtime,
|
|
1157
|
-
|
|
2689
|
+
JS_SetMemoryLimit(runtime, size_option(r_memory_limit, "memory_limit"));
|
|
2690
|
+
data->requested_max_stack_size = size_option(r_max_stack_size, "max_stack_size");
|
|
2691
|
+
JS_SetMaxStackSize(runtime, data->requested_max_stack_size);
|
|
1158
2692
|
|
|
1159
2693
|
register_module_loader_funcs(data);
|
|
1160
2694
|
JS_SetHostPromiseRejectionTracker(runtime, quickjsrb_promise_rejection_tracker, NULL);
|
|
1161
2695
|
js_std_init_handlers(runtime);
|
|
2696
|
+
data->std_handlers_installed = true;
|
|
1162
2697
|
|
|
1163
2698
|
JSValue j_global = JS_GetGlobalObject(data->context);
|
|
1164
2699
|
|
|
@@ -1190,26 +2725,34 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
1190
2725
|
quickjsrb_new_ruby_bridge(data->context, js_quickjsrb_set_timeout, "setTimeout", 2));
|
|
1191
2726
|
}
|
|
1192
2727
|
|
|
2728
|
+
// finish_polyfill_load raises (Ruby longjmp) on a load that fails or
|
|
2729
|
+
// doesn't settle, so nothing holding a JSValue may stay live across the
|
|
2730
|
+
// loads: the free at the bottom of this function would be skipped and
|
|
2731
|
+
// the leaked reference pins its whole object graph past JS_FreeRuntime
|
|
2732
|
+
// (the teardown GC only reclaims what's internally referenced, and the
|
|
2733
|
+
// gc_obj_list assert is compiled out by -DNDEBUG). Drop the global here
|
|
2734
|
+
// and re-acquire it below.
|
|
2735
|
+
JS_FreeValue(data->context, j_global);
|
|
2736
|
+
|
|
1193
2737
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillFileId))))
|
|
1194
2738
|
{
|
|
1195
|
-
|
|
1196
|
-
JS_FreeValue(data->context, j_polyfillFileResult);
|
|
2739
|
+
finish_polyfill_load(data, load_polyfill_bytecode(data, &qjsc_polyfill_file_min, qjsc_polyfill_file_min_size, false));
|
|
1197
2740
|
|
|
1198
2741
|
quickjsrb_init_file_proxy(data);
|
|
1199
2742
|
}
|
|
1200
2743
|
|
|
1201
2744
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillEncodingId))))
|
|
1202
2745
|
{
|
|
1203
|
-
|
|
1204
|
-
JS_FreeValue(data->context, j_polyfillEncodingResult);
|
|
2746
|
+
finish_polyfill_load(data, load_polyfill_bytecode(data, &qjsc_polyfill_encoding_min, qjsc_polyfill_encoding_min_size, false));
|
|
1205
2747
|
}
|
|
1206
2748
|
|
|
1207
2749
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillUrlId))))
|
|
1208
2750
|
{
|
|
1209
|
-
|
|
1210
|
-
JS_FreeValue(data->context, j_polyfillUrlResult);
|
|
2751
|
+
finish_polyfill_load(data, load_polyfill_bytecode(data, &qjsc_polyfill_url_min, qjsc_polyfill_url_min_size, false));
|
|
1211
2752
|
}
|
|
1212
2753
|
|
|
2754
|
+
j_global = JS_GetGlobalObject(data->context);
|
|
2755
|
+
|
|
1213
2756
|
if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillCryptoId))))
|
|
1214
2757
|
{
|
|
1215
2758
|
quickjsrb_init_crypto(data->context, j_global);
|
|
@@ -1218,7 +2761,7 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
1218
2761
|
// console and the remaining host callbacks are registered below this
|
|
1219
2762
|
// point. setTimeout and the File proxy above predate the GVL-released
|
|
1220
2763
|
// polyfill loads only under the audit described at
|
|
1221
|
-
//
|
|
2764
|
+
// run_bytecode_release_gvl — re-read it before reordering this function
|
|
1222
2765
|
// or registering anything else above the loads.
|
|
1223
2766
|
JSValue j_console = JS_NewObject(data->context);
|
|
1224
2767
|
JS_SetPropertyStr(
|
|
@@ -1246,29 +2789,64 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
|
|
|
1246
2789
|
static int interrupt_handler(JSRuntime *runtime, void *opaque)
|
|
1247
2790
|
{
|
|
1248
2791
|
EvalTime *eval_time = opaque;
|
|
1249
|
-
|
|
1250
|
-
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
1251
|
-
int64_t elapsed_ms = (int64_t)(now.tv_sec - eval_time->started_at.tv_sec) * 1000
|
|
1252
|
-
+ (now.tv_nsec - eval_time->started_at.tv_nsec) / 1000000;
|
|
1253
|
-
return elapsed_ms >= eval_time->limit_ms ? 1 : 0;
|
|
2792
|
+
return eval_elapsed_ms(eval_time) >= eval_time->limit_ms ? 1 : 0;
|
|
1254
2793
|
}
|
|
1255
2794
|
|
|
1256
|
-
|
|
2795
|
+
// The result of an evaluation, owned for as long as it takes to convert.
|
|
2796
|
+
struct return_value
|
|
2797
|
+
{
|
|
2798
|
+
JSContext *ctx;
|
|
2799
|
+
JSValue j_val;
|
|
2800
|
+
};
|
|
2801
|
+
|
|
2802
|
+
static VALUE to_rb_return_value_body(VALUE r_owned)
|
|
1257
2803
|
{
|
|
1258
|
-
|
|
2804
|
+
struct return_value *owned = (struct return_value *)r_owned;
|
|
2805
|
+
if (JS_VALUE_GET_NORM_TAG(owned->j_val) == JS_TAG_OBJECT && JS_PromiseState(owned->ctx, owned->j_val) != -1)
|
|
1259
2806
|
{
|
|
1260
|
-
JS_FreeValue(ctx, j_val);
|
|
1261
2807
|
VALUE r_error_message = rb_str_new2("An unawaited Promise was returned to the top-level");
|
|
1262
2808
|
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_NO_AWAIT_ERROR), rb_intern("new"), 2, r_error_message, Qnil));
|
|
1263
2809
|
return Qnil;
|
|
1264
2810
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
2811
|
+
return to_rb_value(owned->ctx, owned->j_val);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
static VALUE to_rb_return_value_release(VALUE r_owned)
|
|
2815
|
+
{
|
|
2816
|
+
struct return_value *owned = (struct return_value *)r_owned;
|
|
2817
|
+
JS_FreeValue(owned->ctx, owned->j_val);
|
|
2818
|
+
return Qnil;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// Converting a result runs guest JS — getters, toJSON — and raises on values
|
|
2822
|
+
// with no Ruby equivalent, so the reference has to outlive every exit rather
|
|
2823
|
+
// than being freed after a conversion that may never return. A nested Promise
|
|
2824
|
+
// is the reachable case: the walk raises, and without this the whole graph the
|
|
2825
|
+
// result holds is retained for the life of the VM.
|
|
2826
|
+
static VALUE to_rb_return_value(JSContext *ctx, JSValue j_val)
|
|
2827
|
+
{
|
|
2828
|
+
struct return_value owned = {ctx, j_val};
|
|
2829
|
+
return rb_ensure(to_rb_return_value_body, (VALUE)&owned, to_rb_return_value_release, (VALUE)&owned);
|
|
1268
2830
|
}
|
|
1269
2831
|
|
|
1270
|
-
static void
|
|
2832
|
+
static void check_vm_poisoned(VMData *data)
|
|
1271
2833
|
{
|
|
2834
|
+
if (!NIL_P(data->r_registrar_error))
|
|
2835
|
+
{
|
|
2836
|
+
// Handed back as itself: a Timeout::Error the host asked for is theirs, and
|
|
2837
|
+
// it was only held this long because raising it where it landed would have
|
|
2838
|
+
// unwound through QuickJS.
|
|
2839
|
+
VALUE r_error = data->r_registrar_error;
|
|
2840
|
+
data->r_registrar_error = Qnil;
|
|
2841
|
+
rb_exc_raise(r_error);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
if (data->handle_source_broken)
|
|
2845
|
+
{
|
|
2846
|
+
VALUE r_msg = rb_str_new2("VM is poisoned: SecureRandom.random_number stopped answering with distinct usable integers, so an object could not be given a handle a guest cannot guess. A new VM will refuse in the same way, so fix the source rather than recycling: it is usually a test double still in place.");
|
|
2847
|
+
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_msg, Qnil));
|
|
2848
|
+
}
|
|
2849
|
+
|
|
1272
2850
|
if (data->oom_poisoned)
|
|
1273
2851
|
{
|
|
1274
2852
|
VALUE r_msg = rb_str_new2("VM is poisoned: a previous evaluation hit out-of-memory; further evaluation may segfault. Recreate the Quickjs::VM.");
|
|
@@ -1308,6 +2886,7 @@ static void arm_eval_timer(VMData *data)
|
|
|
1308
2886
|
{
|
|
1309
2887
|
clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
|
|
1310
2888
|
JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
|
|
2889
|
+
data->eval_timer_armed = true;
|
|
1311
2890
|
}
|
|
1312
2891
|
|
|
1313
2892
|
// Pure-path predicate: true when no JS→Ruby bridge can fire during eval
|
|
@@ -1357,7 +2936,7 @@ static void *eval_code_job_run(void *p)
|
|
|
1357
2936
|
JSValue j_codeResult = JS_Eval(job->ctx, job->code, job->code_len, job->filename, eval_flags);
|
|
1358
2937
|
if (job->async_mode)
|
|
1359
2938
|
{
|
|
1360
|
-
JSValue j_awaitedResult =
|
|
2939
|
+
JSValue j_awaitedResult = quickjsrb_host_await(job->ctx, j_codeResult); // frees j_codeResult
|
|
1361
2940
|
job->result = JS_GetPropertyStr(job->ctx, j_awaitedResult, "value");
|
|
1362
2941
|
JS_FreeValue(job->ctx, j_awaitedResult);
|
|
1363
2942
|
}
|
|
@@ -1368,6 +2947,79 @@ static void *eval_code_job_run(void *p)
|
|
|
1368
2947
|
return NULL;
|
|
1369
2948
|
}
|
|
1370
2949
|
|
|
2950
|
+
// Opens a JS entry on this VM, or refuses if another thread already has one
|
|
2951
|
+
// open. README's "one VM, one thread at a time" rule stops being advisory
|
|
2952
|
+
// here: QuickJS contexts have no internal locking, so two threads inside
|
|
2953
|
+
// JS_Eval on the same context corrupt the heap, and since eval can release
|
|
2954
|
+
// the GVL they really do run at the same time rather than interleaving by
|
|
2955
|
+
// accident.
|
|
2956
|
+
//
|
|
2957
|
+
// A mutex would be the wrong instrument twice over. Held across the release
|
|
2958
|
+
// it deadlocks — the unlock lives after rb_thread_call_without_gvl
|
|
2959
|
+
// re-acquires the GVL, so a second thread blocks in pthread_mutex_lock
|
|
2960
|
+
// while holding the GVL and the first can never get back in to release it.
|
|
2961
|
+
// And a plain mutex self-deadlocks on the nesting this codebase relies on,
|
|
2962
|
+
// where an on_log listener inside a released region re-enters the same VM
|
|
2963
|
+
// on the same thread. Comparing the owner has neither problem: it lets
|
|
2964
|
+
// nesting through by construction and needs no lock at all.
|
|
2965
|
+
// Split from the raise so callers holding malloc'd state can test first and
|
|
2966
|
+
// clean up before unwinding (run_gvl_release_region owns input buffers by
|
|
2967
|
+
// the time it asks).
|
|
2968
|
+
static bool js_entry_owned_elsewhere(VMData *data)
|
|
2969
|
+
{
|
|
2970
|
+
return data->evals_in_flight > 0 && data->owner_thread != rb_thread_current();
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
static void refuse_cross_thread_entry(VMData *data)
|
|
2974
|
+
{
|
|
2975
|
+
rb_raise(rb_eThreadError,
|
|
2976
|
+
"cannot use a Quickjs::VM from two threads at once; it is already evaluating on %+" PRIsVALUE,
|
|
2977
|
+
data->owner_thread);
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// Entry-point guard, sitting next to check_disposed. enter_js_entry alone is
|
|
2981
|
+
// not enough: several entry points read or mutate runtime state before they
|
|
2982
|
+
// reach a counted region — JS_IsJobPending in drain_jobs!, arm_eval_timer
|
|
2983
|
+
// almost everywhere, JS_SetModuleLoaderFunc2 in compile_module — and those
|
|
2984
|
+
// touches are already unsafe against another thread's in-flight JS.
|
|
2985
|
+
static void check_js_entry_owner(VMData *data)
|
|
2986
|
+
{
|
|
2987
|
+
if (js_entry_owned_elsewhere(data))
|
|
2988
|
+
refuse_cross_thread_entry(data);
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
static void enter_js_entry(VMData *data)
|
|
2992
|
+
{
|
|
2993
|
+
if (js_entry_owned_elsewhere(data))
|
|
2994
|
+
refuse_cross_thread_entry(data);
|
|
2995
|
+
|
|
2996
|
+
// After the refusal and before the count, in that order for two reasons.
|
|
2997
|
+
// The re-base mutates rt->stack_top, which another thread's in-flight JS is
|
|
2998
|
+
// checking itself against, so a caller about to be turned away must not have
|
|
2999
|
+
// touched it. And rebase_stack_limit reads evals_in_flight to recognise the
|
|
3000
|
+
// outermost entry, which the increment below is about to spoil.
|
|
3001
|
+
rebase_stack_limit(data);
|
|
3002
|
+
|
|
3003
|
+
data->owner_thread = rb_thread_current();
|
|
3004
|
+
data->evals_in_flight++;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
// Balances enter_js_entry. Clearing the owner only as the outermost entry
|
|
3008
|
+
// closes is what lets a VM be handed between threads sequentially, which
|
|
3009
|
+
// the README allows and pool warmers depend on.
|
|
3010
|
+
//
|
|
3011
|
+
// eval_timer_armed deliberately does not drop here. An evaluation is two
|
|
3012
|
+
// counted regions, the run and then the conversion of its result, and the
|
|
3013
|
+
// budget spans both: a getter that outlives it while the result is being
|
|
3014
|
+
// rendered has to be reported, and the count is already back at zero by then.
|
|
3015
|
+
// The flag therefore means "some entry armed this clock", and each entry point
|
|
3016
|
+
// that renders is responsible for arming above the work it renders.
|
|
3017
|
+
static void leave_js_entry(VMData *data)
|
|
3018
|
+
{
|
|
3019
|
+
if (--data->evals_in_flight == 0)
|
|
3020
|
+
data->owner_thread = Qnil;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
1371
3023
|
// A GVL-release region — the one place that owns the handshake required to
|
|
1372
3024
|
// run a pure-C QuickJS job with the GVL released: the save/restore of
|
|
1373
3025
|
// gvl_released_js (restore, not clear, so a region nested through an
|
|
@@ -1404,19 +3056,154 @@ static VALUE gvl_release_region_cleanup(VALUE p)
|
|
|
1404
3056
|
struct gvl_release_region *region = (struct gvl_release_region *)p;
|
|
1405
3057
|
VMData *data = region->data;
|
|
1406
3058
|
data->gvl_released_js = region->prev_gvl_released;
|
|
1407
|
-
data
|
|
3059
|
+
leave_js_entry(data);
|
|
1408
3060
|
data->gvl_release_regions--;
|
|
1409
3061
|
free(region->owned_bufs[0]);
|
|
1410
3062
|
free(region->owned_bufs[1]);
|
|
1411
3063
|
// Frees the result when the interrupt landed after the job ran but
|
|
1412
|
-
// before the run function marked completion.
|
|
1413
|
-
// caller
|
|
1414
|
-
//
|
|
3064
|
+
// before the run function marked completion. Once the result reaches the
|
|
3065
|
+
// caller it is to_rb_return_value's to own, including when an interrupt
|
|
3066
|
+
// lands during the conversion.
|
|
1415
3067
|
if (!region->completed)
|
|
1416
3068
|
JS_FreeValue(data->context, *region->j_result);
|
|
1417
3069
|
return Qnil;
|
|
1418
3070
|
}
|
|
1419
3071
|
|
|
3072
|
+
// The bounds of the calling thread's stack, probed once and remembered. They
|
|
3073
|
+
// do not move for the life of the thread, and the probe is far too expensive
|
|
3074
|
+
// to repeat: on glibc the main thread takes the path that opens
|
|
3075
|
+
// /proc/self/maps and parses it line by line looking for the vma holding
|
|
3076
|
+
// __libc_stack_end, which in a Ruby process with its many mappings measured
|
|
3077
|
+
// at ~68us. That was landing on every outermost entry, against a bare
|
|
3078
|
+
// eval_code('1+1') of ~2.5us.
|
|
3079
|
+
//
|
|
3080
|
+
// Probing once and answering from the cache also settles the fiber case for
|
|
3081
|
+
// free. A Ruby Fiber (and Enumerator, and every fiber scheduler) runs on its
|
|
3082
|
+
// own mmap'd stack outside these bounds, so a frame pointer that falls
|
|
3083
|
+
// outside them is not on this thread's stack and there is nothing to
|
|
3084
|
+
// measure; that answer needs no syscall once the bounds are known.
|
|
3085
|
+
struct thread_stack_bounds
|
|
3086
|
+
{
|
|
3087
|
+
uintptr_t low;
|
|
3088
|
+
uintptr_t high;
|
|
3089
|
+
bool probed;
|
|
3090
|
+
};
|
|
3091
|
+
|
|
3092
|
+
static void probe_thread_stack_bounds(struct thread_stack_bounds *bounds)
|
|
3093
|
+
{
|
|
3094
|
+
bounds->probed = true;
|
|
3095
|
+
bounds->low = 0;
|
|
3096
|
+
bounds->high = 0;
|
|
3097
|
+
#if defined(__APPLE__)
|
|
3098
|
+
pthread_t self = pthread_self();
|
|
3099
|
+
// Apple reports the address one past the top of the stack, growing down.
|
|
3100
|
+
uintptr_t high = (uintptr_t)pthread_get_stackaddr_np(self);
|
|
3101
|
+
size_t size = pthread_get_stacksize_np(self);
|
|
3102
|
+
if (high == 0 || size == 0 || size > high)
|
|
3103
|
+
return;
|
|
3104
|
+
bounds->low = high - size;
|
|
3105
|
+
bounds->high = high;
|
|
3106
|
+
#elif defined(__linux__)
|
|
3107
|
+
pthread_attr_t attr;
|
|
3108
|
+
if (pthread_getattr_np(pthread_self(), &attr) != 0)
|
|
3109
|
+
return;
|
|
3110
|
+
void *base;
|
|
3111
|
+
size_t size;
|
|
3112
|
+
int rc = pthread_attr_getstack(&attr, &base, &size);
|
|
3113
|
+
pthread_attr_destroy(&attr);
|
|
3114
|
+
if (rc != 0 || base == NULL || size == 0)
|
|
3115
|
+
return;
|
|
3116
|
+
bounds->low = (uintptr_t)base;
|
|
3117
|
+
bounds->high = bounds->low + size;
|
|
3118
|
+
#endif
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
// How much stack the calling thread still has below this frame, or 0 when
|
|
3122
|
+
// that cannot be answered: the platform has neither call, the probe failed,
|
|
3123
|
+
// or the frame is not on this thread's stack at all. rebase_stack_limit
|
|
3124
|
+
// reads 0 as "leave this VM alone".
|
|
3125
|
+
static size_t current_thread_stack_headroom(void)
|
|
3126
|
+
{
|
|
3127
|
+
static __thread struct thread_stack_bounds bounds;
|
|
3128
|
+
if (!bounds.probed)
|
|
3129
|
+
probe_thread_stack_bounds(&bounds);
|
|
3130
|
+
if (bounds.high == 0)
|
|
3131
|
+
return 0;
|
|
3132
|
+
|
|
3133
|
+
// Both bounds, not just the lower one. Where the allocator puts a fiber's
|
|
3134
|
+
// stack relative to the thread's is not ours to predict: below it, sp <= low
|
|
3135
|
+
// and a lower-bound check already answered 0; above it, sp - low measures
|
|
3136
|
+
// across unrelated mappings and reports headroom that is not there.
|
|
3137
|
+
uintptr_t sp = (uintptr_t)__builtin_frame_address(0);
|
|
3138
|
+
return (sp > bounds.low && sp < bounds.high) ? (size_t)(sp - bounds.low) : 0;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// Room left for QuickJS to report the overflow and for Ruby to unwind through
|
|
3142
|
+
// the bridge frames above it once it does. The check itself only compares the
|
|
3143
|
+
// frame pointer, so the margin has to cover everything that still has to run
|
|
3144
|
+
// after it fires.
|
|
3145
|
+
#define QUICKJSRB_STACK_MARGIN (256 * 1024)
|
|
3146
|
+
|
|
3147
|
+
// QuickJS latches rt->stack_top from whichever thread called JS_NewRuntime and
|
|
3148
|
+
// never revisits it, so rt->stack_limit (stack_top - stack_size) keeps pointing
|
|
3149
|
+
// into that thread's stack for the life of the VM. Evaluate from a thread whose
|
|
3150
|
+
// stack sits below that limit and js_check_stack_overflow trips on its very
|
|
3151
|
+
// first check, reporting "stack overflow" on code as trivial as 1 + 1.
|
|
3152
|
+
//
|
|
3153
|
+
// Whether it fires is luck of where the OS put the two stacks: on macOS the gap
|
|
3154
|
+
// stays under the 4MB default, so the handoff README.md:472 promises appears to
|
|
3155
|
+
// work, while on Linux every cross-thread eval raises.
|
|
3156
|
+
//
|
|
3157
|
+
// Re-basing alone would trade that for something worse. A Ruby thread's machine
|
|
3158
|
+
// stack is a fraction of the main thread's, so a 4MB budget re-based onto one
|
|
3159
|
+
// outlives the stack it is measuring: Ruby's guard page is reached first and the
|
|
3160
|
+
// eval dies with SystemStackError instead of QuickJS raising. Base and budget
|
|
3161
|
+
// therefore move together, or not at all.
|
|
3162
|
+
//
|
|
3163
|
+
// Not at all is a real case. pthread reports the *thread's* stack, and a Ruby
|
|
3164
|
+
// Fiber runs on its own mmap'd one outside those bounds, as do Enumerator and
|
|
3165
|
+
// every fiber scheduler; the query returns 0 there, and on platforms with
|
|
3166
|
+
// neither call it always does. Re-basing without being able to clamp is the
|
|
3167
|
+
// worst of the three outcomes, so an unmeasurable stack leaves the VM exactly
|
|
3168
|
+
// as it was: still latched to its creating thread, still the pre-existing
|
|
3169
|
+
// behaviour, and no new way to run off the end of a stack.
|
|
3170
|
+
//
|
|
3171
|
+
// Outermost entry only: a nested one (a bridge re-entering its own VM, an
|
|
3172
|
+
// on_log listener evaluating) sits deeper on the same stack, and re-basing
|
|
3173
|
+
// there would hand it a fresh full budget, removing the guard exactly where
|
|
3174
|
+
// runaway recursion is what needs catching.
|
|
3175
|
+
static void rebase_stack_limit(VMData *data)
|
|
3176
|
+
{
|
|
3177
|
+
if (data->evals_in_flight != 0)
|
|
3178
|
+
return;
|
|
3179
|
+
|
|
3180
|
+
size_t headroom = current_thread_stack_headroom();
|
|
3181
|
+
|
|
3182
|
+
// A headroom under the margin is not a small budget, it is an unusable
|
|
3183
|
+
// answer: there would be no room left to report the overflow with. musl
|
|
3184
|
+
// makes that the normal case rather than a corner one, because its
|
|
3185
|
+
// main-thread pthread_attr_getstack reports only the currently mapped part
|
|
3186
|
+
// of the stack rather than what RLIMIT_STACK allows, so early on the answer
|
|
3187
|
+
// is a few pages. Clamping to what is left there set stack_limit one byte
|
|
3188
|
+
// below stack_top and every eval raised "stack overflow", 1 + 1 included.
|
|
3189
|
+
//
|
|
3190
|
+
// So this joins the zero case: an answer we cannot use leaves the VM exactly
|
|
3191
|
+
// as it was, latched to its creating thread. Cross-thread handoff stays
|
|
3192
|
+
// broken on such a platform, which is the pre-existing behaviour, rather
|
|
3193
|
+
// than the platform breaking outright.
|
|
3194
|
+
if (headroom <= QUICKJSRB_STACK_MARGIN)
|
|
3195
|
+
return;
|
|
3196
|
+
|
|
3197
|
+
JSRuntime *runtime = JS_GetRuntime(data->context);
|
|
3198
|
+
JS_UpdateStackTop(runtime);
|
|
3199
|
+
|
|
3200
|
+
if (data->requested_max_stack_size == 0)
|
|
3201
|
+
return; // caller asked for no limit; honour it
|
|
3202
|
+
|
|
3203
|
+
size_t usable = headroom - QUICKJSRB_STACK_MARGIN;
|
|
3204
|
+
JS_SetMaxStackSize(runtime, usable < data->requested_max_stack_size ? usable : data->requested_max_stack_size);
|
|
3205
|
+
}
|
|
3206
|
+
|
|
1420
3207
|
// Run job_run(job) with the GVL released. owned_buf0/1 are malloc'd
|
|
1421
3208
|
// buffers backing the job's inputs; ownership transfers to the region,
|
|
1422
3209
|
// which frees them on every exit path — including the disposed bail-out
|
|
@@ -1425,13 +3212,17 @@ static VALUE gvl_release_region_cleanup(VALUE p)
|
|
|
1425
3212
|
// dispose!), so re-check here: nothing between this check and the release
|
|
1426
3213
|
// yields, and dispose! refuses while evals_in_flight > 0, so the two
|
|
1427
3214
|
// sides can't miss each other.
|
|
3215
|
+
|
|
1428
3216
|
static void run_gvl_release_region(VMData *data, void *(*job_run)(void *), void *job, JSValue *j_result, void *owned_buf0, void *owned_buf1)
|
|
1429
3217
|
{
|
|
1430
|
-
|
|
3218
|
+
// Both refusals have to happen before the buffers are handed to the
|
|
3219
|
+
// region, since the rb_ensure that would free them is not armed yet.
|
|
3220
|
+
if (data->disposed || js_entry_owned_elsewhere(data))
|
|
1431
3221
|
{
|
|
1432
3222
|
free(owned_buf0);
|
|
1433
3223
|
free(owned_buf1);
|
|
1434
|
-
check_disposed(data);
|
|
3224
|
+
check_disposed(data); // raises when disposed
|
|
3225
|
+
refuse_cross_thread_entry(data); // otherwise it was the owner check
|
|
1435
3226
|
}
|
|
1436
3227
|
|
|
1437
3228
|
struct gvl_release_region region = {
|
|
@@ -1444,7 +3235,7 @@ static void run_gvl_release_region(VMData *data, void *(*job_run)(void *), void
|
|
|
1444
3235
|
.completed = false,
|
|
1445
3236
|
};
|
|
1446
3237
|
|
|
1447
|
-
data
|
|
3238
|
+
enter_js_entry(data); // cannot raise: the check above already passed
|
|
1448
3239
|
data->gvl_release_regions++;
|
|
1449
3240
|
data->gvl_released_js = true;
|
|
1450
3241
|
rb_ensure(gvl_release_region_run, (VALUE)®ion, gvl_release_region_cleanup, (VALUE)®ion);
|
|
@@ -1466,7 +3257,7 @@ static void check_no_gvl_release_in_flight(VMData *data)
|
|
|
1466
3257
|
|
|
1467
3258
|
static VALUE evals_in_flight_release(VALUE p)
|
|
1468
3259
|
{
|
|
1469
|
-
((VMData *)p)
|
|
3260
|
+
leave_js_entry((VMData *)p);
|
|
1470
3261
|
return Qnil;
|
|
1471
3262
|
}
|
|
1472
3263
|
|
|
@@ -1487,16 +3278,100 @@ static VALUE evals_in_flight_release(VALUE p)
|
|
|
1487
3278
|
static VALUE run_held_js_entry(VMData *data, VALUE (*body)(VALUE), VALUE arg)
|
|
1488
3279
|
{
|
|
1489
3280
|
check_disposed(data);
|
|
1490
|
-
|
|
3281
|
+
// Raises before the ensure is armed, which is safe here: unlike the
|
|
3282
|
+
// release region, this path owns no malloc'd state at this point.
|
|
3283
|
+
enter_js_entry(data);
|
|
1491
3284
|
return rb_ensure(body, arg, evals_in_flight_release, (VALUE)data);
|
|
1492
3285
|
}
|
|
1493
3286
|
|
|
1494
|
-
|
|
3287
|
+
// Result conversion runs guest JS — getters, toJSON, a Proxy trap — and the
|
|
3288
|
+
// bridges that code can reach yield the GVL, so it has to be a counted JS entry
|
|
3289
|
+
// like every other execution. Without it a dispose! from a getter is granted
|
|
3290
|
+
// while the walk is still reading the context it frees (#81). The entry closes
|
|
3291
|
+
// before the conversion's own value is handed back, which is why these wrap the
|
|
3292
|
+
// conversion rather than the whole tail.
|
|
3293
|
+
static VALUE to_rb_return_value_held_body(VALUE r_owned)
|
|
3294
|
+
{
|
|
3295
|
+
struct return_value *owned = (struct return_value *)r_owned;
|
|
3296
|
+
return to_rb_return_value(owned->ctx, owned->j_val);
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// The owning ensure is armed inside the entry, not around it, so that the
|
|
3300
|
+
// result is freed while the entry is still held — freeing it outside would
|
|
3301
|
+
// reopen the window this exists to close. That leaves j_val unowned if
|
|
3302
|
+
// run_held_js_entry itself raises, which it cannot here: these tails run on
|
|
3303
|
+
// the thread that produced the value, with no GVL yield since the producing
|
|
3304
|
+
// entry closed, so neither `disposed` nor `owner_thread` can have changed.
|
|
3305
|
+
static VALUE to_rb_return_value_held(VMData *data, JSValue j_val)
|
|
3306
|
+
{
|
|
3307
|
+
struct return_value owned = {data->context, j_val};
|
|
3308
|
+
return run_held_js_entry(data, to_rb_return_value_held_body, (VALUE)&owned);
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
static VALUE js_exception_held_body(VALUE r_owned)
|
|
3312
|
+
{
|
|
3313
|
+
struct return_value *owned = (struct return_value *)r_owned;
|
|
3314
|
+
return to_rb_value(owned->ctx, owned->j_val);
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
// For the tails whose value is the JS_EXCEPTION sentinel: converting it pulls
|
|
3318
|
+
// the pending exception and raises. Unlike to_rb_return_value_held this does
|
|
3319
|
+
// not own its argument, which is why it takes the sentinel rather than a
|
|
3320
|
+
// JSValue in general — the sentinel carries no reference to release.
|
|
3321
|
+
static VALUE raise_from_js_exception_held(VMData *data)
|
|
3322
|
+
{
|
|
3323
|
+
struct return_value owned = {data->context, JS_EXCEPTION};
|
|
3324
|
+
return run_held_js_entry(data, js_exception_held_body, (VALUE)&owned);
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
// run_held_js_entry that ends the microtask checkpoint after body, raise or not.
|
|
3328
|
+
struct held_checkpoint_call
|
|
3329
|
+
{
|
|
3330
|
+
VMData *data;
|
|
3331
|
+
VALUE (*body)(VALUE);
|
|
3332
|
+
VALUE arg;
|
|
3333
|
+
};
|
|
3334
|
+
|
|
3335
|
+
static VALUE held_checkpoint_end(VALUE p)
|
|
1495
3336
|
{
|
|
1496
|
-
|
|
3337
|
+
VMData *data = ((struct held_checkpoint_call *)p)->data;
|
|
3338
|
+
// Only the outermost entry: a nested one returns while outer JS is still
|
|
3339
|
+
// running, and this also keeps the handler from re-entering the checkpoint.
|
|
3340
|
+
if (data->evals_in_flight == 1)
|
|
3341
|
+
quickjsrb_end_microtask_checkpoint(data);
|
|
1497
3342
|
return Qnil;
|
|
1498
3343
|
}
|
|
1499
3344
|
|
|
3345
|
+
static VALUE held_checkpoint_body(VALUE p)
|
|
3346
|
+
{
|
|
3347
|
+
struct held_checkpoint_call *call = (struct held_checkpoint_call *)p;
|
|
3348
|
+
return rb_ensure(call->body, call->arg, held_checkpoint_end, p);
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
static VALUE run_held_js_checkpoint_entry(VMData *data, VALUE (*body)(VALUE), VALUE arg)
|
|
3352
|
+
{
|
|
3353
|
+
struct held_checkpoint_call call = {data, body, arg};
|
|
3354
|
+
return run_held_js_entry(data, held_checkpoint_body, (VALUE)&call);
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
static VALUE eval_code_job_run_body(VALUE p)
|
|
3358
|
+
{
|
|
3359
|
+
struct eval_code_job *job = (struct eval_code_job *)p;
|
|
3360
|
+
eval_code_job_run(job);
|
|
3361
|
+
return to_rb_return_value(job->ctx, job->result);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// Unwraps js_std_await's {value, done}, or raises the JS exception.
|
|
3365
|
+
static VALUE bytecode_eval_result_to_rb(JSContext *ctx, JSValue j_result)
|
|
3366
|
+
{
|
|
3367
|
+
if (JS_IsException(j_result))
|
|
3368
|
+
return to_rb_value(ctx, j_result); // raises
|
|
3369
|
+
|
|
3370
|
+
JSValue j_returnedValue = JS_GetPropertyStr(ctx, j_result, "value");
|
|
3371
|
+
JS_FreeValue(ctx, j_result);
|
|
3372
|
+
return to_rb_return_value(ctx, j_returnedValue);
|
|
3373
|
+
}
|
|
3374
|
+
|
|
1500
3375
|
// rb_ensure bodies over the shared bytecode core, for the GVL-held call
|
|
1501
3376
|
// sites: vm_m_loadPolyfillBytecode's held fallback loads without awaiting
|
|
1502
3377
|
// (matching its release path), vm_m_evalBytecode awaits the result. These
|
|
@@ -1512,11 +3387,8 @@ static VALUE bytecode_load_body(VALUE p)
|
|
|
1512
3387
|
static VALUE bytecode_eval_await_body(VALUE p)
|
|
1513
3388
|
{
|
|
1514
3389
|
struct bytecode_load_job *job = (struct bytecode_load_job *)p;
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
// the JS_ReadObject short-circuit — through untouched.
|
|
1518
|
-
job->result = js_std_await(job->ctx, job->result);
|
|
1519
|
-
return Qnil;
|
|
3390
|
+
bytecode_eval_await_job_run(job);
|
|
3391
|
+
return bytecode_eval_result_to_rb(job->ctx, job->result);
|
|
1520
3392
|
}
|
|
1521
3393
|
|
|
1522
3394
|
// Copy a Ruby String to a malloc'd buffer that outlives a GVL release —
|
|
@@ -1567,86 +3439,533 @@ static VALUE eval_code_release_gvl(VMData *data, VALUE r_code, const char *filen
|
|
|
1567
3439
|
};
|
|
1568
3440
|
run_gvl_release_region(data, eval_code_job_run, &job, &job.result, code_buf, filename_buf);
|
|
1569
3441
|
|
|
1570
|
-
return
|
|
3442
|
+
return to_rb_return_value_held(data, job.result);
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
static VALUE vm_m_evalCode(int argc, VALUE *argv, VALUE r_self)
|
|
3446
|
+
{
|
|
3447
|
+
VMData *data;
|
|
3448
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
3449
|
+
|
|
3450
|
+
check_disposed(data);
|
|
3451
|
+
check_vm_poisoned(data);
|
|
3452
|
+
check_js_entry_owner(data);
|
|
3453
|
+
|
|
3454
|
+
VALUE r_code, r_opts;
|
|
3455
|
+
rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
|
|
3456
|
+
const char *filename = parse_code_and_filename(r_code, r_opts);
|
|
3457
|
+
|
|
3458
|
+
bool async_mode = true;
|
|
3459
|
+
if (!NIL_P(r_opts))
|
|
3460
|
+
{
|
|
3461
|
+
VALUE r_async = rb_hash_aref(r_opts, ID2SYM(rb_intern("async")));
|
|
3462
|
+
if (r_async == Qfalse)
|
|
3463
|
+
async_mode = false;
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
// Argument parsing allocates, so it can yield the GVL to a concurrent
|
|
3467
|
+
// dispose! that leaves data->context dangling — and arm_eval_timer
|
|
3468
|
+
// dereferences the runtime, which puts it ahead of both the release
|
|
3469
|
+
// region's own re-check and run_held_js_entry's. Nothing between here and
|
|
3470
|
+
// the release yields (StringValue can't coerce: parse_code_and_filename
|
|
3471
|
+
// already refused a non-String), and dispose! refuses once either of those
|
|
3472
|
+
// has counted us, so re-checking here closes the window on both paths.
|
|
3473
|
+
check_disposed(data);
|
|
3474
|
+
// Ownership needs the same re-check as disposal, and for the same reason:
|
|
3475
|
+
// the parsing above yields, so another thread can have taken the VM since
|
|
3476
|
+
// the check at entry. arm_eval_timer writes the shared eval_time, so
|
|
3477
|
+
// without this it would reset the budget of the eval that thread is
|
|
3478
|
+
// already running — the counted regions below refuse, but only after.
|
|
3479
|
+
check_js_entry_owner(data);
|
|
3480
|
+
arm_eval_timer(data);
|
|
3481
|
+
|
|
3482
|
+
StringValue(r_code);
|
|
3483
|
+
|
|
3484
|
+
if (can_eval_gvl_free(data))
|
|
3485
|
+
return eval_code_release_gvl(data, r_code, filename, async_mode);
|
|
3486
|
+
|
|
3487
|
+
// Bridged path: a JS→Ruby bridge (define_function / module loader /
|
|
3488
|
+
// setTimeout / File / crypto) may fire mid-eval, so keep the GVL held and
|
|
3489
|
+
// run the shared eval core directly. With the GVL held there's no
|
|
3490
|
+
// compaction risk, so RSTRING_PTR is usable without a malloc'd copy.
|
|
3491
|
+
struct eval_code_job job = {
|
|
3492
|
+
.ctx = data->context,
|
|
3493
|
+
.code = RSTRING_PTR(r_code),
|
|
3494
|
+
.code_len = (size_t)RSTRING_LEN(r_code),
|
|
3495
|
+
.filename = filename,
|
|
3496
|
+
.async_mode = async_mode,
|
|
3497
|
+
.result = JS_UNDEFINED,
|
|
3498
|
+
};
|
|
3499
|
+
// No checkpoint on the GVL-free path: it is not taken with a handler set.
|
|
3500
|
+
return run_held_js_checkpoint_entry(data, eval_code_job_run_body, (VALUE)&job);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
struct compile_job
|
|
3504
|
+
{
|
|
3505
|
+
JSContext *ctx;
|
|
3506
|
+
const char *code;
|
|
3507
|
+
size_t code_len;
|
|
3508
|
+
const char *filename;
|
|
3509
|
+
JSValue result;
|
|
3510
|
+
};
|
|
3511
|
+
|
|
3512
|
+
// Parses to a function object and stops there. Like the preload read, and
|
|
3513
|
+
// unlike the eval core above, this runs no JS at all, so its release needs
|
|
3514
|
+
// no can_eval_gvl_free gate — there is no top level that could reach a Ruby
|
|
3515
|
+
// bridge, on any VM. JS_EVAL_TYPE_GLOBAL is what carries that argument:
|
|
3516
|
+
// __JS_EvalInternal only resolves imports for the JSModuleDef it builds
|
|
3517
|
+
// under JS_EVAL_TYPE_MODULE, so no module loader can fire here either. A
|
|
3518
|
+
// module compile is a different story and gets no such freedom. Same
|
|
3519
|
+
// MUST-NOT-touch-Ruby constraint as every other released job.
|
|
3520
|
+
static void *compile_job_run(void *p)
|
|
3521
|
+
{
|
|
3522
|
+
struct compile_job *job = p;
|
|
3523
|
+
job->result = JS_Eval(job->ctx, job->code, job->code_len, job->filename,
|
|
3524
|
+
JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
3525
|
+
return NULL;
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
// Inputs are copied because RSTRING_PTR can be invalidated by GC compaction
|
|
3529
|
+
// while we're released, same as eval_code_release_gvl.
|
|
3530
|
+
static JSValue compile_release_gvl(VMData *data, VALUE r_code, const char *filename)
|
|
3531
|
+
{
|
|
3532
|
+
size_t code_len;
|
|
3533
|
+
char *code_buf = copy_rstring_to_owned_buffer(r_code, &code_len, true);
|
|
3534
|
+
|
|
3535
|
+
char *filename_buf = strdup(filename);
|
|
3536
|
+
if (filename_buf == NULL)
|
|
3537
|
+
{
|
|
3538
|
+
free(code_buf);
|
|
3539
|
+
rb_raise(rb_eNoMemError, "failed to allocate compile filename buffer");
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
struct compile_job job = {
|
|
3543
|
+
.ctx = data->context,
|
|
3544
|
+
.code = code_buf,
|
|
3545
|
+
.code_len = code_len,
|
|
3546
|
+
.filename = filename_buf,
|
|
3547
|
+
.result = JS_UNDEFINED,
|
|
3548
|
+
};
|
|
3549
|
+
run_gvl_release_region(data, compile_job_run, &job, &job.result, code_buf, filename_buf);
|
|
3550
|
+
|
|
3551
|
+
return job.result;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
struct bytecode_serialize_job
|
|
3555
|
+
{
|
|
3556
|
+
VMData *data;
|
|
3557
|
+
JSValue j_compiled; // owned here; see compiled_to_bytecode_string for the rule
|
|
3558
|
+
uint8_t *out_buf;
|
|
3559
|
+
const char *failure_message;
|
|
3560
|
+
};
|
|
3561
|
+
|
|
3562
|
+
static VALUE bytecode_serialize_body(VALUE p)
|
|
3563
|
+
{
|
|
3564
|
+
struct bytecode_serialize_job *job = (struct bytecode_serialize_job *)p;
|
|
3565
|
+
|
|
3566
|
+
size_t out_len;
|
|
3567
|
+
job->out_buf = JS_WriteObject(job->data->context, &out_len, job->j_compiled, JS_WRITE_OBJ_BYTECODE);
|
|
3568
|
+
if (job->out_buf == NULL)
|
|
3569
|
+
{
|
|
3570
|
+
VALUE r_msg = rb_str_new2(job->failure_message);
|
|
3571
|
+
rb_exc_raise(rb_funcall(QUICKJSRB_ERROR_FOR(QUICKJSRB_ROOT_RUNTIME_ERROR), rb_intern("new"), 2, r_msg, Qnil));
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
VALUE r_bytecode = rb_str_new((const char *)job->out_buf, (long)out_len);
|
|
3575
|
+
rb_enc_associate(r_bytecode, rb_ascii8bit_encoding());
|
|
3576
|
+
return rb_obj_freeze(r_bytecode);
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
static VALUE bytecode_serialize_cleanup(VALUE p)
|
|
3580
|
+
{
|
|
3581
|
+
struct bytecode_serialize_job *job = (struct bytecode_serialize_job *)p;
|
|
3582
|
+
|
|
3583
|
+
JS_FreeValue(job->data->context, job->j_compiled);
|
|
3584
|
+
if (job->out_buf != NULL)
|
|
3585
|
+
js_free(job->data->context, job->out_buf);
|
|
3586
|
+
return Qnil;
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
static VALUE bytecode_serialize_held(VALUE p)
|
|
3590
|
+
{
|
|
3591
|
+
return rb_ensure(bytecode_serialize_body, p, bytecode_serialize_cleanup, p);
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// Serializes a compiled function or module into a frozen ASCII-8BIT String.
|
|
3595
|
+
// Shared by both compile entry points, and a GVL-held JS entry rather than
|
|
3596
|
+
// plain inline code for two reasons: rb_str_new allocates, which is a thread
|
|
3597
|
+
// switch point, so without evals_in_flight elevated a concurrent dispose!
|
|
3598
|
+
// could free the context between JS_WriteObject and the js_free below; and
|
|
3599
|
+
// the blob JS_WriteObject hands back is js_malloc'd, so it has to go home
|
|
3600
|
+
// through js_free to keep the runtime's memory accounting straight — the
|
|
3601
|
+
// ensure gets it there however the body exits, including an async interrupt
|
|
3602
|
+
// landing in rb_str_new.
|
|
3603
|
+
//
|
|
3604
|
+
// Ownership of j_compiled transfers here in full, but "consumes it" is not
|
|
3605
|
+
// quite the whole rule, because run_held_js_entry's disposed check raises
|
|
3606
|
+
// ahead of the rb_ensure that does the freeing. That path frees nothing, and
|
|
3607
|
+
// must not: dispose! sets disposed before handing the teardown to
|
|
3608
|
+
// JS_FreeRuntime, so by the time the check fires j_compiled's storage is
|
|
3609
|
+
// either already reclaimed or being reclaimed concurrently with the GVL
|
|
3610
|
+
// released, and a JS_FreeValue aimed at it would be a use-after-free rather
|
|
3611
|
+
// than a cleanup. Nothing leaks either way — the runtime took it. So: freed
|
|
3612
|
+
// through the context on every exit while the VM is alive, abandoned to the
|
|
3613
|
+
// teardown once it is not. Unreachable today in any case, since neither
|
|
3614
|
+
// caller yields the GVL between its compile and this call.
|
|
3615
|
+
static VALUE compiled_to_bytecode_string(VMData *data, JSValue j_compiled, const char *failure_message)
|
|
3616
|
+
{
|
|
3617
|
+
struct bytecode_serialize_job job = {
|
|
3618
|
+
.data = data,
|
|
3619
|
+
.j_compiled = j_compiled,
|
|
3620
|
+
.out_buf = NULL,
|
|
3621
|
+
.failure_message = failure_message,
|
|
3622
|
+
};
|
|
3623
|
+
return run_held_js_entry(data, bytecode_serialize_held, (VALUE)&job);
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
struct js_exception_job
|
|
3627
|
+
{
|
|
3628
|
+
VMData *data;
|
|
3629
|
+
JSValue j_exception;
|
|
3630
|
+
};
|
|
3631
|
+
|
|
3632
|
+
static VALUE js_exception_body(VALUE p)
|
|
3633
|
+
{
|
|
3634
|
+
struct js_exception_job *job = (struct js_exception_job *)p;
|
|
3635
|
+
return to_rb_value(job->data->context, job->j_exception); // raises
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// Turns a thrown JSValue into the Ruby exception it raises, as a counted JS
|
|
3639
|
+
// entry rather than a bare call. Converting an error is JS execution: it
|
|
3640
|
+
// reads name, message and stack off the thrown object, and any of the three
|
|
3641
|
+
// can be an accessor inherited from a prototype the VM's own code has
|
|
3642
|
+
// replaced. So a parse failure on a VM where something did
|
|
3643
|
+
//
|
|
3644
|
+
// Object.defineProperty(SyntaxError.prototype, 'name', { get: () => boom() })
|
|
3645
|
+
//
|
|
3646
|
+
// runs boom() during conversion — and with evals_in_flight back at zero after
|
|
3647
|
+
// the release region, a dispose! from that bridge was granted and the rest of
|
|
3648
|
+
// the conversion ran against a freed context. Reproducible SIGSEGV.
|
|
3649
|
+
//
|
|
3650
|
+
// The parse half of a compile is counted and the serialize half is counted;
|
|
3651
|
+
// this is the third tail out of the same function, and it was the one left
|
|
3652
|
+
// bare.
|
|
3653
|
+
static VALUE js_exception_to_rb(VMData *data, JSValue j_exception)
|
|
3654
|
+
{
|
|
3655
|
+
struct js_exception_job job = {
|
|
3656
|
+
.data = data,
|
|
3657
|
+
.j_exception = j_exception,
|
|
3658
|
+
};
|
|
3659
|
+
return run_held_js_entry(data, js_exception_body, (VALUE)&job);
|
|
1571
3660
|
}
|
|
1572
3661
|
|
|
1573
|
-
static VALUE
|
|
3662
|
+
static VALUE vm_m_compile(int argc, VALUE *argv, VALUE r_self)
|
|
1574
3663
|
{
|
|
1575
3664
|
VMData *data;
|
|
1576
3665
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1577
3666
|
|
|
1578
3667
|
check_disposed(data);
|
|
1579
|
-
|
|
3668
|
+
check_vm_poisoned(data);
|
|
3669
|
+
check_js_entry_owner(data);
|
|
1580
3670
|
|
|
1581
3671
|
VALUE r_code, r_opts;
|
|
1582
3672
|
rb_scan_args(argc, argv, "1:", &r_code, &r_opts);
|
|
1583
3673
|
const char *filename = parse_code_and_filename(r_code, r_opts);
|
|
1584
3674
|
|
|
1585
|
-
|
|
1586
|
-
|
|
3675
|
+
// Re-check for the same reason vm_m_evalCode does: argument parsing can
|
|
3676
|
+
// yield to a concurrent dispose!, and arm_eval_timer is the first thing
|
|
3677
|
+
// here to touch the context.
|
|
3678
|
+
//
|
|
3679
|
+
// Arming buys a compile nothing: the parser never calls js_poll_interrupts
|
|
3680
|
+
// — every call site is in the interpreter, for-in, instanceof or regexp
|
|
3681
|
+
// exec — so timeout_msec does not bound a compile, and a pathological
|
|
3682
|
+
// source parses for as long as it takes. Releasing the GVL at least keeps
|
|
3683
|
+
// that off every other Ruby thread. It is kept only so an outermost
|
|
3684
|
+
// compile starts from a fresh clock rather than inheriting a lapsed one.
|
|
3685
|
+
//
|
|
3686
|
+
// Nested, it is worse than useless: a compile issued from a bridge
|
|
3687
|
+
// callback (a define_function proc, an on_log listener) would overwrite
|
|
3688
|
+
// the enclosing eval's started_at on every call, so interrupt_handler
|
|
3689
|
+
// never sees the elapsed limit and the enclosing eval never times out at
|
|
3690
|
+
// all. vm_m_loadPolyfillBytecode conditions on the same counter for the
|
|
3691
|
+
// same reason.
|
|
3692
|
+
//
|
|
3693
|
+
// Not a file-wide invariant yet, to be clear. vm_m_evalCode,
|
|
3694
|
+
// vm_m_evalBytecode, call_global_function_body, vm_m_import and
|
|
3695
|
+
// vm_m_drainJobs all still arm unconditionally and all still defeat
|
|
3696
|
+
// timeout_msec the same way — call_global_function_body above its path
|
|
3697
|
+
// resolution, so a nested call that fails to resolve has already reset the
|
|
3698
|
+
// enclosing clock. Hoisting the condition into arm_eval_timer
|
|
3699
|
+
// would close them together, but it would also start interrupting
|
|
3700
|
+
// workloads that re-enter the VM from a bridge and today run unbounded —
|
|
3701
|
+
// a semantics change that deserves its own PR rather than a ride on this
|
|
3702
|
+
// one.
|
|
3703
|
+
check_disposed(data);
|
|
3704
|
+
// Same re-check as vm_m_evalCode, for the same reason: parsing yielded, so
|
|
3705
|
+
// the owner may have changed since the check at entry.
|
|
3706
|
+
check_js_entry_owner(data);
|
|
3707
|
+
if (data->evals_in_flight == 0)
|
|
3708
|
+
arm_eval_timer(data);
|
|
3709
|
+
|
|
3710
|
+
// A guaranteed no-op: parse_code_and_filename already refused a
|
|
3711
|
+
// non-String, so no to_str can run here and open a yield point between
|
|
3712
|
+
// the check above and the release below.
|
|
3713
|
+
StringValue(r_code);
|
|
3714
|
+
// Serializing stays on the GVL-held side of the region: the region frees
|
|
3715
|
+
// its buffers with free(), while JS_WriteObject's blob has to go back
|
|
3716
|
+
// through js_free, and teaching it a second kind of ownership for one
|
|
3717
|
+
// caller isn't worth it. Parsing dominates anyway — leaving the serialize
|
|
3718
|
+
// behind only pulls the two-thread ratio from ~0.5 to ~0.55.
|
|
3719
|
+
JSValue j_func = compile_release_gvl(data, r_code, filename);
|
|
3720
|
+
if (JS_IsException(j_func))
|
|
1587
3721
|
{
|
|
1588
|
-
|
|
1589
|
-
if (r_async == Qfalse)
|
|
1590
|
-
async_mode = false;
|
|
3722
|
+
return js_exception_to_rb(data, j_func); // raises Ruby exception
|
|
1591
3723
|
}
|
|
1592
3724
|
|
|
1593
|
-
|
|
3725
|
+
return compiled_to_bytecode_string(data, j_func, "failed to serialize compiled bytecode");
|
|
3726
|
+
}
|
|
1594
3727
|
|
|
1595
|
-
|
|
3728
|
+
// Stands in for the real loader while compiling a module to bytecode.
|
|
3729
|
+
//
|
|
3730
|
+
// __JS_EvalInternal runs js_resolve_module before it honors
|
|
3731
|
+
// JS_EVAL_FLAG_COMPILE_ONLY, so compiling `import { x } from 'dep'` tries to
|
|
3732
|
+
// load 'dep' — which would make a module impossible to compile without its
|
|
3733
|
+
// whole dependency graph on hand. Nothing about that resolution survives:
|
|
3734
|
+
// JS_WriteModule stores req_module_entries as the specifier written in the
|
|
3735
|
+
// source, never the module it resolved to, and export names aren't checked
|
|
3736
|
+
// until js_link_module at evaluation time. So an empty stub satisfies the
|
|
3737
|
+
// compile and leaves no trace in the blob; the importing VM resolves the real
|
|
3738
|
+
// dependency through its own loader.
|
|
3739
|
+
//
|
|
3740
|
+
// The stubs do land in the compiling context's module map, so this must only
|
|
3741
|
+
// ever run on a throwaway VM — otherwise a later real import of 'dep' on that
|
|
3742
|
+
// VM would find the empty stub.
|
|
3743
|
+
static JSModuleDef *quickjsrb_stub_module_loader(JSContext *ctx, const char *module_name, void *opaque, JSValueConst attributes)
|
|
3744
|
+
{
|
|
3745
|
+
static const char *empty_module = "export {};";
|
|
3746
|
+
JSValue j_stub = JS_Eval(ctx, empty_module, strlen(empty_module), module_name,
|
|
3747
|
+
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
3748
|
+
if (JS_IsException(j_stub))
|
|
3749
|
+
return NULL;
|
|
1596
3750
|
|
|
1597
|
-
|
|
1598
|
-
|
|
3751
|
+
JSModuleDef *m = JS_VALUE_GET_PTR(j_stub);
|
|
3752
|
+
JS_FreeValue(ctx, j_stub);
|
|
3753
|
+
return m;
|
|
3754
|
+
}
|
|
1599
3755
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
3756
|
+
// Compiles source as an ES module and serializes it. Unlike vm_m_compile,
|
|
3757
|
+
// `name` is not a debug label: js_read_module rebuilds the JSModuleDef under
|
|
3758
|
+
// the name baked in here, so it is the module's identity in every VM that
|
|
3759
|
+
// later reads this blob. Quickjs.register_module keys the registry by that
|
|
3760
|
+
// same string, which is what keeps the two from drifting apart.
|
|
3761
|
+
//
|
|
3762
|
+
// Stays private, and must: the stub loader below is swapped in at the
|
|
3763
|
+
// *runtime* level for the duration, so this assumes exclusive use of the VM.
|
|
3764
|
+
// Quickjs._compile_registered_module honors that by compiling on a disposable
|
|
3765
|
+
// VM it owns; exposing this publicly would let a compile race an import on a
|
|
3766
|
+
// shared VM and resolve it against stubs.
|
|
3767
|
+
struct compile_module_job
|
|
3768
|
+
{
|
|
3769
|
+
VMData *data;
|
|
3770
|
+
VALUE r_code;
|
|
3771
|
+
const char *name;
|
|
3772
|
+
};
|
|
3773
|
+
|
|
3774
|
+
static VALUE compile_module_body(VALUE p);
|
|
3775
|
+
|
|
3776
|
+
static VALUE vm_m_compileModule(VALUE r_self, VALUE r_code, VALUE r_name)
|
|
3777
|
+
{
|
|
3778
|
+
VMData *data;
|
|
3779
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
3780
|
+
|
|
3781
|
+
check_disposed(data);
|
|
3782
|
+
check_vm_poisoned(data);
|
|
3783
|
+
check_js_entry_owner(data);
|
|
3784
|
+
Check_Type(r_code, T_STRING);
|
|
3785
|
+
Check_Type(r_name, T_STRING);
|
|
3786
|
+
|
|
3787
|
+
// Hoisted above everything that touches the runtime. Check_Type already
|
|
3788
|
+
// ruled out a to_str, but StringValueCStr still calls rb_str_modify to
|
|
3789
|
+
// NUL-terminate a shared or embedded-NUL-free substring, and that
|
|
3790
|
+
// allocates — the same thread switch point the rest of this file treats as
|
|
3791
|
+
// a yield. Left where it read most naturally, inline in the JS_Eval
|
|
3792
|
+
// arguments, a concurrent dispose! landing there would free the runtime
|
|
3793
|
+
// out from under the loader swap and the eval. Coercing first puts the
|
|
3794
|
+
// disposed check after the last yield and before the first context access,
|
|
3795
|
+
// as on the other JS entry points.
|
|
3796
|
+
const char *name = StringValueCStr(r_name);
|
|
3797
|
+
|
|
3798
|
+
check_disposed(data);
|
|
3799
|
+
// Re-checked after the coercion above yields, and then immediately turned
|
|
3800
|
+
// into a claim by run_held_js_entry below: unlike the other compile paths,
|
|
3801
|
+
// the runtime work here is a full JS_Eval plus a runtime-level loader swap,
|
|
3802
|
+
// so a check that recorded nothing would leave both of those exposed to a
|
|
3803
|
+
// second thread passing the same idle check.
|
|
3804
|
+
check_js_entry_owner(data);
|
|
3805
|
+
// Guarded for the reason spelled out in vm_m_compile: only the outermost
|
|
3806
|
+
// JS entry point owns the timeout budget. Nesting can't happen on this
|
|
3807
|
+
// path today — it is private and both callers compile on a disposable VM
|
|
3808
|
+
// they own — but the two compile entry points arming differently would be
|
|
3809
|
+
// a difference with no reason behind it.
|
|
3810
|
+
// Armed before the claim below, not after: the condition reads the counter
|
|
3811
|
+
// that run_held_js_entry is about to raise, so claiming first would skip
|
|
3812
|
+
// the arm on every outermost compile.
|
|
3813
|
+
if (data->evals_in_flight == 0)
|
|
3814
|
+
arm_eval_timer(data);
|
|
3815
|
+
|
|
3816
|
+
struct compile_module_job job = {
|
|
3817
|
+
.data = data,
|
|
3818
|
+
.r_code = r_code,
|
|
3819
|
+
.name = name,
|
|
1611
3820
|
};
|
|
1612
|
-
run_held_js_entry(data,
|
|
1613
|
-
return to_rb_return_value(data->context, job.result);
|
|
3821
|
+
return run_held_js_entry(data, compile_module_body, (VALUE)&job);
|
|
1614
3822
|
}
|
|
1615
3823
|
|
|
1616
|
-
static VALUE
|
|
3824
|
+
static VALUE compile_module_body(VALUE p)
|
|
3825
|
+
{
|
|
3826
|
+
struct compile_module_job *job = (struct compile_module_job *)p;
|
|
3827
|
+
VMData *data = job->data;
|
|
3828
|
+
VALUE r_code = job->r_code;
|
|
3829
|
+
|
|
3830
|
+
JS_SetModuleLoaderFunc2(JS_GetRuntime(data->context), NULL, quickjsrb_stub_module_loader,
|
|
3831
|
+
js_module_check_attributes, NULL);
|
|
3832
|
+
// RSTRING_PTR raw, where the released paths copy — the copy there is for GC
|
|
3833
|
+
// movability, and the NUL that quickjs.h:835 asks for comes free with it.
|
|
3834
|
+
// Held here, the raw pointer carries that NUL anyway: Ruby only shares one
|
|
3835
|
+
// String's buffer with another when the substring runs to the parent's end,
|
|
3836
|
+
// so ptr[len] is always somebody's terminator. Worth stating because the
|
|
3837
|
+
// lexer's comment and regexp scanners test for the NUL before the end
|
|
3838
|
+
// pointer, so a buffer without one would let a source ending mid-comment
|
|
3839
|
+
// read on into whatever follows it in memory.
|
|
3840
|
+
JSValue j_mod = JS_Eval(data->context, RSTRING_PTR(r_code), RSTRING_LEN(r_code), job->name,
|
|
3841
|
+
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
3842
|
+
register_module_loader_funcs(data);
|
|
3843
|
+
if (JS_IsException(j_mod))
|
|
3844
|
+
return js_exception_to_rb(data, j_mod); // raises
|
|
3845
|
+
|
|
3846
|
+
return compiled_to_bytecode_string(data, j_mod, "failed to serialize compiled module bytecode");
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
// Reads module bytecode into this context. The read registers the JSModuleDef
|
|
3850
|
+
// in ctx->loaded_modules but does not evaluate it: evaluation still happens on
|
|
3851
|
+
// first import, and js_resolve_module recurses into it from the importer, so
|
|
3852
|
+
// no loader hook is involved for a preloaded name.
|
|
3853
|
+
//
|
|
3854
|
+
// The name is recorded in preloaded_module_names for the normalize hook's
|
|
3855
|
+
// short-circuit, and returned so the caller can confirm the blob was filed
|
|
3856
|
+
// where it asked.
|
|
3857
|
+
//
|
|
3858
|
+
// `name` is the name the caller expects the blob to carry. Reading the same
|
|
3859
|
+
// module into a context twice can't be undone — js_read_module appends a
|
|
3860
|
+
// second JSModuleDef under the same name, js_find_loaded_module returns the
|
|
3861
|
+
// first one it walks past, and the orphan holds its bytecode until the
|
|
3862
|
+
// context is freed — so a name already preloaded here skips the read
|
|
3863
|
+
// entirely. That also makes this idempotent, which the caller relies on: it
|
|
3864
|
+
// is legitimate to preload the same module into a VM more than once.
|
|
3865
|
+
static VALUE vm_m_preloadModuleBytecode(VALUE r_self, VALUE r_bytecode, VALUE r_name)
|
|
1617
3866
|
{
|
|
1618
3867
|
VMData *data;
|
|
1619
3868
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1620
3869
|
|
|
1621
3870
|
check_disposed(data);
|
|
3871
|
+
check_vm_poisoned(data);
|
|
3872
|
+
check_js_entry_owner(data);
|
|
1622
3873
|
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
3874
|
+
// Strict String rather than StringValue's coercion, matching
|
|
3875
|
+
// vm_m_evalBytecode: bytecode is a binary blob, so there's no sensible
|
|
3876
|
+
// to_str-able stand-in for one, and refusing the coercion means no yield
|
|
3877
|
+
// point can open up between the checks above and the context access below.
|
|
3878
|
+
if (!RB_TYPE_P(r_bytecode, T_STRING))
|
|
3879
|
+
{
|
|
3880
|
+
VALUE r_class = rb_class_name(CLASS_OF(r_bytecode));
|
|
3881
|
+
rb_raise(rb_eTypeError, "Bytecode must be a String, got %s", StringValueCStr(r_class));
|
|
3882
|
+
}
|
|
3883
|
+
Check_Type(r_name, T_STRING);
|
|
3884
|
+
|
|
3885
|
+
// A module name crosses into QuickJS as raw bytes and comes back binary, so
|
|
3886
|
+
// it is compared and keyed on bytes here. Encoding-aware equality would call
|
|
3887
|
+
// a non-ASCII name different from the identical name the caller passed in
|
|
3888
|
+
// (UTF-8 from the registry), and this is also the form the normalize hook
|
|
3889
|
+
// looks up, since it builds its specifier from a C string too.
|
|
3890
|
+
VALUE r_key = rb_str_new(RSTRING_PTR(r_name), RSTRING_LEN(r_name));
|
|
3891
|
+
|
|
3892
|
+
if (RTEST(rb_hash_aref(data->preloaded_module_names, r_key)))
|
|
3893
|
+
return r_name;
|
|
3894
|
+
|
|
3895
|
+
// Deserialize with the GVL released. The read touches no Ruby and no
|
|
3896
|
+
// bridge — it only registers the def, deferring linking to import — so it
|
|
3897
|
+
// releases unconditionally (no can_eval_gvl_free gate): a pool of
|
|
3898
|
+
// per-thread VMs each preloading the same modules deserializes in parallel
|
|
3899
|
+
// instead of serializing through the GVL. RSTRING_PTR is GC-movable while
|
|
3900
|
+
// released, so the bytecode is copied to a buffer the region owns and frees.
|
|
3901
|
+
//
|
|
3902
|
+
// An async interrupt in the region's GVL re-acquire frees j_mod but leaves
|
|
3903
|
+
// the def js_new_module_def already registered in loaded_modules, untracked
|
|
3904
|
+
// by preloaded_module_names. That is reachable on a VM the caller keeps —
|
|
3905
|
+
// import(names, from:) preloads into a live VM, not just VM.new into a
|
|
3906
|
+
// half-built one — so it is worth spelling out what a retry does: the
|
|
3907
|
+
// untracked name misses the guard above, a second def is read under the same
|
|
3908
|
+
// name, js_find_loaded_module returns the first, and the retry records the
|
|
3909
|
+
// name. Behaviour recovers, at the cost of one orphan holding its bytecode
|
|
3910
|
+
// for the life of the context, so unregistering on interrupt isn't worth it.
|
|
3911
|
+
size_t buf_len;
|
|
3912
|
+
uint8_t *buf = (uint8_t *)copy_rstring_to_owned_buffer(r_bytecode, &buf_len, false);
|
|
3913
|
+
// The read runs no JS, so there is nothing here to budget — but a failed read
|
|
3914
|
+
// is rendered, and rendering reads name off a SyntaxError whose prototype the
|
|
3915
|
+
// guest may have given a throwing getter. That render must not run on the
|
|
3916
|
+
// clock a previous entry left behind. Conditional as in vm_m_compile: nested
|
|
3917
|
+
// under a bridge, the enclosing eval's budget governs.
|
|
3918
|
+
if (data->evals_in_flight == 0)
|
|
3919
|
+
arm_eval_timer(data);
|
|
3920
|
+
JSValue j_mod = run_bytecode_release_gvl(data, bytecode_read_job_run, buf, buf_len, true);
|
|
3921
|
+
if (JS_IsException(j_mod))
|
|
3922
|
+
return raise_from_js_exception_held(data); // raises
|
|
1628
3923
|
|
|
1629
|
-
|
|
1630
|
-
JSValue j_func = JS_Eval(data->context, RSTRING_PTR(r_code), RSTRING_LEN(r_code), filename,
|
|
1631
|
-
JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_ASYNC | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
1632
|
-
if (JS_IsException(j_func))
|
|
3924
|
+
if (JS_VALUE_GET_TAG(j_mod) != JS_TAG_MODULE)
|
|
1633
3925
|
{
|
|
1634
|
-
|
|
3926
|
+
JS_FreeValue(data->context, j_mod);
|
|
3927
|
+
rb_raise(rb_eTypeError, "bytecode is not an ES module (compile it with type: :module)");
|
|
1635
3928
|
}
|
|
1636
3929
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
3930
|
+
js_module_set_import_meta(data->context, j_mod, FALSE, FALSE);
|
|
3931
|
+
|
|
3932
|
+
JSAtom j_baked = JS_GetModuleName(data->context, JS_VALUE_GET_PTR(j_mod));
|
|
3933
|
+
const char *baked = JS_AtomToCString(data->context, j_baked);
|
|
3934
|
+
VALUE r_baked = rb_str_new_cstr(baked ? baked : "");
|
|
3935
|
+
if (baked)
|
|
3936
|
+
JS_FreeCString(data->context, baked);
|
|
3937
|
+
JS_FreeAtom(data->context, j_baked);
|
|
3938
|
+
|
|
3939
|
+
// The module def is owned by ctx->loaded_modules (js_new_module_def leaves
|
|
3940
|
+
// it there with ref_count 1); JS_NewModuleValue dup'd it for us.
|
|
3941
|
+
JS_FreeValue(data->context, j_mod);
|
|
3942
|
+
|
|
3943
|
+
// A blob whose baked name disagrees with the name it was filed under would
|
|
3944
|
+
// be resolvable only by the baked one, so the guard above would never see
|
|
3945
|
+
// it again and every preload would read another copy. There is no way to
|
|
3946
|
+
// take the read back, so the VM is already polluted: raise and let the
|
|
3947
|
+
// caller discard it.
|
|
3948
|
+
if (!RTEST(rb_str_equal(r_baked, r_key)))
|
|
1641
3949
|
{
|
|
1642
|
-
|
|
1643
|
-
|
|
3950
|
+
rb_raise(rb_eArgError, "module bytecode is named %"PRIsVALUE", not %"PRIsVALUE,
|
|
3951
|
+
rb_str_inspect(r_baked), rb_str_inspect(r_name));
|
|
1644
3952
|
}
|
|
1645
3953
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
3954
|
+
rb_hash_aset(data->preloaded_module_names, rb_str_freeze(r_key), Qtrue);
|
|
3955
|
+
return r_name;
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
// Number of sources the normalize hook is holding for the load hook to pick
|
|
3959
|
+
// up. Exposed only so tests can assert the cache doesn't accumulate entries
|
|
3960
|
+
// nothing will ever collect: when a loader resolves a specifier onto a
|
|
3961
|
+
// preloaded canonical, QuickJS finds the module and never calls the load hook,
|
|
3962
|
+
// so a source stashed for that name would sit there for the VM's lifetime.
|
|
3963
|
+
static VALUE vm_m_pendingModuleSourceCount(VALUE r_self)
|
|
3964
|
+
{
|
|
3965
|
+
VMData *data;
|
|
3966
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
3967
|
+
|
|
3968
|
+
return LONG2NUM(RHASH_SIZE(data->module_source_cache));
|
|
1650
3969
|
}
|
|
1651
3970
|
|
|
1652
3971
|
static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
@@ -1655,7 +3974,8 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1655
3974
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1656
3975
|
|
|
1657
3976
|
check_disposed(data);
|
|
1658
|
-
|
|
3977
|
+
check_vm_poisoned(data);
|
|
3978
|
+
check_js_entry_owner(data);
|
|
1659
3979
|
|
|
1660
3980
|
if (!RB_TYPE_P(r_bytecode, T_STRING))
|
|
1661
3981
|
{
|
|
@@ -1667,22 +3987,38 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1667
3987
|
|
|
1668
3988
|
arm_eval_timer(data);
|
|
1669
3989
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
3990
|
+
JSValue j_result;
|
|
3991
|
+
if (can_eval_gvl_free(data))
|
|
3992
|
+
{
|
|
3993
|
+
// Pure VM: no JS→Ruby bridge can fire during the run or the await's
|
|
3994
|
+
// job drain, so run load + await with the GVL released — warmer pools
|
|
3995
|
+
// executing one compiled bundle across per-thread VMs recover
|
|
3996
|
+
// multi-core scaling instead of serializing every run through the
|
|
3997
|
+
// GVL. The bytecode is copied because RSTRING_PTR is GC-movable
|
|
3998
|
+
// while the GVL is released.
|
|
3999
|
+
size_t buf_len;
|
|
4000
|
+
uint8_t *buf = (uint8_t *)copy_rstring_to_owned_buffer(r_bytecode, &buf_len, false);
|
|
4001
|
+
j_result = run_bytecode_release_gvl(data, bytecode_eval_await_job_run, buf, buf_len, true);
|
|
4002
|
+
}
|
|
4003
|
+
else
|
|
4004
|
+
{
|
|
4005
|
+
// Bridged path: user bytecode may invoke Ruby-bridged callbacks
|
|
4006
|
+
// registered via define_function, which call Ruby APIs — keep the GVL
|
|
4007
|
+
// held. With it held there's no compaction risk, so RSTRING_PTR is
|
|
4008
|
+
// usable without a copy.
|
|
4009
|
+
struct bytecode_load_job job = {data->context,
|
|
4010
|
+
(const uint8_t *)RSTRING_PTR(r_bytecode),
|
|
4011
|
+
(size_t)RSTRING_LEN(r_bytecode),
|
|
4012
|
+
JS_UNDEFINED};
|
|
4013
|
+
return run_held_js_checkpoint_entry(data, bytecode_eval_await_body, (VALUE)&job);
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
if (JS_IsException(j_result))
|
|
4017
|
+
return raise_from_js_exception_held(data); // raises
|
|
1682
4018
|
|
|
1683
|
-
JSValue j_returnedValue = JS_GetPropertyStr(data->context,
|
|
1684
|
-
JS_FreeValue(data->context,
|
|
1685
|
-
return
|
|
4019
|
+
JSValue j_returnedValue = JS_GetPropertyStr(data->context, j_result, "value");
|
|
4020
|
+
JS_FreeValue(data->context, j_result);
|
|
4021
|
+
return to_rb_return_value_held(data, j_returnedValue);
|
|
1686
4022
|
}
|
|
1687
4023
|
|
|
1688
4024
|
// Loads pre-compiled polyfill bytecode without arming the eval timer.
|
|
@@ -1709,13 +4045,21 @@ static VALUE vm_m_loadPolyfillBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1709
4045
|
VMData *data;
|
|
1710
4046
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
1711
4047
|
|
|
1712
|
-
//
|
|
1713
|
-
//
|
|
1714
|
-
//
|
|
1715
|
-
|
|
4048
|
+
// The disposed check has to sit after the last GVL-yield point and before
|
|
4049
|
+
// the context is touched. Refusing a to_str-able stand-in removes the yield
|
|
4050
|
+
// instead of ordering around it: bytecode is a binary blob, so coercion buys
|
|
4051
|
+
// nothing, and without it there is no window for a concurrent dispose! to
|
|
4052
|
+
// free the context between the check and the load. Same guard as
|
|
4053
|
+
// vm_m_evalBytecode and _preload_module_bytecode.
|
|
4054
|
+
if (!RB_TYPE_P(r_bytecode, T_STRING))
|
|
4055
|
+
{
|
|
4056
|
+
VALUE r_class = rb_class_name(CLASS_OF(r_bytecode));
|
|
4057
|
+
rb_raise(rb_eTypeError, "Bytecode must be a String, got %s", StringValueCStr(r_class));
|
|
4058
|
+
}
|
|
1716
4059
|
|
|
1717
4060
|
check_disposed(data);
|
|
1718
|
-
|
|
4061
|
+
check_vm_poisoned(data);
|
|
4062
|
+
check_js_entry_owner(data);
|
|
1719
4063
|
|
|
1720
4064
|
// "Unbudgeted" needs enforcing, not just skipping arm_eval_timer: the
|
|
1721
4065
|
// interrupt handler installed by a previous eval stays armed with that
|
|
@@ -1728,7 +4072,10 @@ static VALUE vm_m_loadPolyfillBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1728
4072
|
// from inside a bridge callback (e.g. an on_log listener) must stay
|
|
1729
4073
|
// under the in-flight eval's budget, not erase it.
|
|
1730
4074
|
if (data->evals_in_flight == 0)
|
|
4075
|
+
{
|
|
1731
4076
|
JS_SetInterruptHandler(JS_GetRuntime(data->context), NULL, NULL);
|
|
4077
|
+
data->eval_timer_armed = false;
|
|
4078
|
+
}
|
|
1732
4079
|
|
|
1733
4080
|
size_t buf_len = (size_t)RSTRING_LEN(r_bytecode);
|
|
1734
4081
|
JSValue j_result;
|
|
@@ -1752,155 +4099,234 @@ static VALUE vm_m_loadPolyfillBytecode(VALUE r_self, VALUE r_bytecode)
|
|
|
1752
4099
|
j_result = job.result;
|
|
1753
4100
|
}
|
|
1754
4101
|
|
|
1755
|
-
|
|
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
|
-
|
|
1773
|
-
JS_FreeValue(data->context, j_result);
|
|
4102
|
+
finish_polyfill_load(data, j_result); // raises unless settled fulfilled
|
|
1774
4103
|
return Qnil;
|
|
1775
4104
|
}
|
|
1776
4105
|
|
|
1777
|
-
|
|
4106
|
+
struct define_function_call
|
|
1778
4107
|
{
|
|
1779
|
-
|
|
1780
|
-
|
|
4108
|
+
VMData *data;
|
|
1781
4109
|
VALUE r_name;
|
|
1782
4110
|
VALUE r_flags;
|
|
1783
4111
|
VALUE r_block;
|
|
1784
|
-
|
|
4112
|
+
};
|
|
1785
4113
|
|
|
1786
|
-
|
|
1787
|
-
|
|
4114
|
+
// What a define holds while it resolves its path: the parent it is walking
|
|
4115
|
+
// and the two values the bridge function captures. Owned by an ensure from
|
|
4116
|
+
// before any of them exists, because resolution runs guest JS — the first
|
|
4117
|
+
// segment is an eval and every later one a property read, any of which can
|
|
4118
|
+
// be a getter that reaches a bridge, raises, or outlives the budget — and the
|
|
4119
|
+
// install itself can run a setter. Every one of those unwinds through here.
|
|
4120
|
+
struct define_function_install
|
|
4121
|
+
{
|
|
4122
|
+
JSContext *ctx;
|
|
4123
|
+
JSValue j_parent;
|
|
4124
|
+
JSValue ruby_data[2];
|
|
4125
|
+
};
|
|
1788
4126
|
|
|
1789
|
-
|
|
1790
|
-
|
|
4127
|
+
static VALUE define_function_release(VALUE p)
|
|
4128
|
+
{
|
|
4129
|
+
struct define_function_install *install = (struct define_function_install *)p;
|
|
4130
|
+
JS_FreeValue(install->ctx, install->j_parent);
|
|
4131
|
+
JS_FreeValue(install->ctx, install->ruby_data[0]);
|
|
4132
|
+
JS_FreeValue(install->ctx, install->ruby_data[1]);
|
|
4133
|
+
return Qnil;
|
|
4134
|
+
}
|
|
1791
4135
|
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
4136
|
+
struct define_function_run
|
|
4137
|
+
{
|
|
4138
|
+
struct define_function_call *call;
|
|
4139
|
+
struct define_function_install *install;
|
|
4140
|
+
VALUE r_segs; // the segments as Strings, stringified once, outside the region
|
|
4141
|
+
VALUE r_key_sym; // the key the bridge looks the block up by
|
|
4142
|
+
};
|
|
1797
4143
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
4144
|
+
// Two answers a resolution step can give that are not the object asked for,
|
|
4145
|
+
// and they are different failures. A pending exception is the guest's own —
|
|
4146
|
+
// a bridge raising in a getter, the budget lapsing, a dispose! reached from
|
|
4147
|
+
// the path — and folding it into "not an object" both blamed the caller's
|
|
4148
|
+
// path for it and left the Ruby exception behind it parked in alive_objects
|
|
4149
|
+
// for the life of the VM. Only a value that really is not an object is the
|
|
4150
|
+
// caller's mistake.
|
|
4151
|
+
static void check_path_segment(JSContext *ctx, JSValueConst j_val, const char *segment)
|
|
4152
|
+
{
|
|
4153
|
+
if (JS_IsException(j_val))
|
|
4154
|
+
raise_js_exception(ctx);
|
|
4155
|
+
if (!JS_IsObject(j_val))
|
|
4156
|
+
rb_raise(rb_eArgError, "cannot define function: '%s' is not an object", segment);
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
static VALUE define_function_resolve_and_install(VALUE p)
|
|
4160
|
+
{
|
|
4161
|
+
struct define_function_run *run = (struct define_function_run *)p;
|
|
4162
|
+
VMData *data = run->call->data;
|
|
4163
|
+
JSContext *ctx = data->context;
|
|
4164
|
+
struct define_function_install *install = run->install;
|
|
4165
|
+
VALUE r_segs = run->r_segs;
|
|
4166
|
+
long path_len = RARRAY_LEN(r_segs);
|
|
4167
|
+
|
|
4168
|
+
install->ruby_data[0] = JS_NewInt64(ctx, (int64_t)SYM2ID(run->r_key_sym));
|
|
4169
|
+
install->ruby_data[1] = JS_NewBool(ctx, RTEST(rb_funcall(run->call->r_flags, rb_intern("include?"), 1, ID2SYM(rb_intern("async")))));
|
|
1804
4170
|
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
// For multi-element arrays, traverse path[0..n-2] using JS_Eval for the first
|
|
1824
|
-
// segment (so lexical const/let bindings are resolved, not just global properties)
|
|
1825
|
-
// and JS_GetPropertyStr for subsequent segments.
|
|
1826
|
-
JSValue j_parent;
|
|
1827
|
-
if (path_len == 1)
|
|
4171
|
+
if (path_len > 1)
|
|
4172
|
+
{
|
|
4173
|
+
// Resolving the path runs guest JS under whatever interrupt handler the
|
|
4174
|
+
// previous entry left armed. Refresh the clock, or a lapsed budget fires in
|
|
4175
|
+
// the first getter and is reported as that getter's fault. Unconditional,
|
|
4176
|
+
// as it always was here: nested under a bridge this resets the enclosing
|
|
4177
|
+
// eval's clock, which vm_m_compile's comment records as a class deferred to
|
|
4178
|
+
// its own PR rather than something to change one entry point at a time.
|
|
4179
|
+
arm_eval_timer(data);
|
|
4180
|
+
|
|
4181
|
+
// The first segment is not a lookup, it is an eval: `myLib` can be a
|
|
4182
|
+
// lexical binding as well as a global property, and either can be an
|
|
4183
|
+
// accessor.
|
|
4184
|
+
const char *first_seg = StringValueCStr(RARRAY_AREF(r_segs, 0));
|
|
4185
|
+
install->j_parent = JS_Eval(ctx, first_seg, strlen(first_seg), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
|
|
4186
|
+
check_path_segment(ctx, install->j_parent, first_seg);
|
|
4187
|
+
|
|
4188
|
+
for (long i = 1; i < path_len - 1; i++)
|
|
1828
4189
|
{
|
|
1829
|
-
|
|
4190
|
+
const char *seg = StringValueCStr(RARRAY_AREF(r_segs, i));
|
|
4191
|
+
JSValue j_next = JS_GetPropertyStr(ctx, install->j_parent, seg);
|
|
4192
|
+
JS_FreeValue(ctx, install->j_parent);
|
|
4193
|
+
install->j_parent = j_next;
|
|
4194
|
+
check_path_segment(ctx, j_next, seg);
|
|
1830
4195
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
4196
|
+
}
|
|
4197
|
+
else
|
|
4198
|
+
{
|
|
4199
|
+
// A plain name never armed before, and the only JS it can run is a setter
|
|
4200
|
+
// on globalThis. It gets a clock of its own only as the outermost entry:
|
|
4201
|
+
// nested under a bridge, the enclosing eval's budget governs, and arming
|
|
4202
|
+
// here unconditionally let a bridge that defined a function on every
|
|
4203
|
+
// call keep the enclosing eval alive past its timeout_msec.
|
|
4204
|
+
if (data->evals_in_flight == 1)
|
|
1838
4205
|
arm_eval_timer(data);
|
|
1839
|
-
|
|
4206
|
+
install->j_parent = JS_GetGlobalObject(ctx);
|
|
4207
|
+
}
|
|
1840
4208
|
|
|
1841
|
-
|
|
1842
|
-
{
|
|
1843
|
-
JS_FreeValue(data->context, j_parent);
|
|
1844
|
-
JS_FreeValue(data->context, ruby_data[0]);
|
|
1845
|
-
JS_FreeValue(data->context, ruby_data[1]);
|
|
1846
|
-
rb_raise(rb_eArgError, "cannot define function: '%s' is not an object", first_seg);
|
|
1847
|
-
}
|
|
4209
|
+
const char *funcName = StringValueCStr(RARRAY_AREF(r_segs, path_len - 1));
|
|
1848
4210
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
4211
|
+
JSValue j_func = JS_NewCFunctionData(ctx, js_quickjsrb_call_global, 1, 0, 2, install->ruby_data);
|
|
4212
|
+
if (JS_IsException(j_func))
|
|
4213
|
+
return raise_js_exception(ctx);
|
|
4214
|
+
|
|
4215
|
+
// JS_SetPropertyStr consumes j_func on every path and, with JS_PROP_THROW,
|
|
4216
|
+
// answers -1 with the TypeError pending when the target refuses — a frozen
|
|
4217
|
+
// parent, a frozen globalThis, a setter that throws. Ignored, that was a
|
|
4218
|
+
// define that reported success for a function which did not exist, and a
|
|
4219
|
+
// stale exception left on the context for whatever ran next.
|
|
4220
|
+
if (JS_SetPropertyStr(ctx, install->j_parent, funcName, j_func) < 0)
|
|
4221
|
+
return raise_js_exception(ctx);
|
|
4222
|
+
|
|
4223
|
+
// Recorded only now that the property is installed. can_eval_gvl_free reads
|
|
4224
|
+
// this table as "a bridge can fire during eval", and an entry left behind by
|
|
4225
|
+
// a define that failed took the VM off the GVL-release path for good.
|
|
4226
|
+
rb_hash_aset(data->defined_functions, run->r_key_sym, run->call->r_block);
|
|
4227
|
+
return Qnil;
|
|
4228
|
+
}
|
|
1854
4229
|
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
rb_raise(rb_eArgError, "cannot define function: '%s' is not an object", StringValueCStr(r_seg_str));
|
|
1861
|
-
}
|
|
1862
|
-
j_parent = j_next;
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
4230
|
+
static VALUE define_global_function_body(VALUE p)
|
|
4231
|
+
{
|
|
4232
|
+
struct define_function_call *call = (struct define_function_call *)p;
|
|
4233
|
+
VMData *data = call->data;
|
|
4234
|
+
VALUE r_name = call->r_name;
|
|
1865
4235
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
rb_ary_push(r_result, rb_funcall(RARRAY_AREF(r_name, i), rb_intern("to_sym"), 0));
|
|
1876
|
-
return r_result;
|
|
1877
|
-
}
|
|
1878
|
-
else if (SYMBOL_P(r_name) || RB_TYPE_P(r_name, T_STRING))
|
|
4236
|
+
// A plain name is a path of one segment whose parent is the global object;
|
|
4237
|
+
// the only difference is what the caller gets back.
|
|
4238
|
+
bool as_path = RB_TYPE_P(r_name, T_ARRAY);
|
|
4239
|
+
VALUE r_path = as_path ? r_name : rb_ary_new3(1, r_name);
|
|
4240
|
+
long path_len = RARRAY_LEN(r_path);
|
|
4241
|
+
if (path_len < 1)
|
|
4242
|
+
rb_raise(rb_eArgError, "function's path array must not be empty");
|
|
4243
|
+
|
|
4244
|
+
for (long i = 0; i < path_len; i++)
|
|
1879
4245
|
{
|
|
1880
|
-
VALUE
|
|
4246
|
+
VALUE r_seg = RARRAY_AREF(r_path, i);
|
|
4247
|
+
if (!(SYMBOL_P(r_seg) || RB_TYPE_P(r_seg, T_STRING)))
|
|
4248
|
+
rb_raise(rb_eTypeError, "function's name should be a Symbol or a String");
|
|
4249
|
+
}
|
|
1881
4250
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
4251
|
+
// Stringified once, here, and handed to the resolution as Strings: to_s on
|
|
4252
|
+
// a caller's object is a yield point and can raise, and neither belongs
|
|
4253
|
+
// inside the region that holds JS values. The bridge looks its block up by
|
|
4254
|
+
// the segments joined with ".", e.g. ["myLib", "hello"] -> :"myLib.hello",
|
|
4255
|
+
// and a plain name as itself.
|
|
4256
|
+
VALUE r_segs = rb_ary_new();
|
|
4257
|
+
for (long i = 0; i < path_len; i++)
|
|
4258
|
+
rb_ary_push(r_segs, rb_funcall(RARRAY_AREF(r_path, i), rb_intern("to_s"), 0));
|
|
4259
|
+
VALUE r_key_sym = rb_funcall(rb_funcall(r_segs, rb_intern("join"), 1, rb_str_new2(".")), rb_intern("to_sym"), 0);
|
|
4260
|
+
|
|
4261
|
+
struct define_function_install install = {data->context, JS_UNDEFINED, {JS_UNDEFINED, JS_UNDEFINED}};
|
|
4262
|
+
struct define_function_run run = {call, &install, r_segs, r_key_sym};
|
|
4263
|
+
rb_ensure(define_function_resolve_and_install, (VALUE)&run, define_function_release, (VALUE)&install);
|
|
4264
|
+
|
|
4265
|
+
if (!as_path)
|
|
4266
|
+
return r_key_sym;
|
|
4267
|
+
|
|
4268
|
+
VALUE r_result = rb_ary_new();
|
|
4269
|
+
for (long i = 0; i < path_len; i++)
|
|
4270
|
+
rb_ary_push(r_result, rb_funcall(RARRAY_AREF(r_segs, i), rb_intern("to_sym"), 0));
|
|
4271
|
+
return r_result;
|
|
4272
|
+
}
|
|
1885
4273
|
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
4274
|
+
// Registering a function is a JS entry point like any other, and was the one
|
|
4275
|
+
// that never said so. Two independent problems land on this call, and routing
|
|
4276
|
+
// the body through run_held_js_entry is what closes both.
|
|
4277
|
+
//
|
|
4278
|
+
// Lifetime: the body resolves the path with JS_Eval and JS_GetPropertyStr,
|
|
4279
|
+
// either of which can land on an accessor property and run user JS that calls
|
|
4280
|
+
// a bridge — and it interleaves that with rb_funcall(to_s) on caller-supplied
|
|
4281
|
+
// objects, which yields the GVL. With evals_in_flight left at zero throughout,
|
|
4282
|
+
// dispose! did not refuse:
|
|
4283
|
+
//
|
|
4284
|
+
// vm.define_function('boom') { vm.dispose!; 1 }
|
|
4285
|
+
// vm.eval_code("Object.defineProperty(globalThis, 'myLib', { get() { boom(); return {}; } }); 0")
|
|
4286
|
+
// vm.define_function(['myLib', 'hello']) { 1 } # SIGSEGV
|
|
4287
|
+
//
|
|
4288
|
+
// dispose! succeeds from inside the getter, and the traversal then runs
|
|
4289
|
+
// JS_GetPropertyStr and JS_FreeValue against a freed JSContext. Elevating the
|
|
4290
|
+
// counter for the whole body is what every other JS entry point already does,
|
|
4291
|
+
// and it turns that into the ThreadError dispose! owes the caller.
|
|
4292
|
+
//
|
|
4293
|
+
// Concurrency: that same GVL yield lets a second thread in. A bare owner check
|
|
4294
|
+
// would pass on an idle VM and record nothing, so the second thread passes the
|
|
4295
|
+
// same idle check and walks into JS_Eval alongside the traversal. Only
|
|
4296
|
+
// claiming the VM closes that.
|
|
4297
|
+
//
|
|
4298
|
+
// Neither reason subsumes the other — one is a single thread freeing the
|
|
4299
|
+
// context under its own traversal, the other is two threads inside JS_Eval at
|
|
4300
|
+
// once — so retiring one of them is not grounds for unwinding the routing.
|
|
4301
|
+
//
|
|
4302
|
+
// The absent check_js_entry_owner beside check_disposed is deliberate, not the
|
|
4303
|
+
// oversight it looks like next to the other six entry points: this is the one
|
|
4304
|
+
// where nothing touches the runtime before the counted region, so the claim
|
|
4305
|
+
// inside run_held_js_entry is the whole guard. check_no_gvl_release_in_flight
|
|
4306
|
+
// and rb_scan_args above it read no runtime state; anything added there that
|
|
4307
|
+
// does needs the explicit check back.
|
|
4308
|
+
static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
4309
|
+
{
|
|
4310
|
+
rb_need_block();
|
|
1889
4311
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
JS_FreeValue(data->context, j_global);
|
|
1895
|
-
JS_FreeValue(data->context, ruby_data[0]);
|
|
1896
|
-
JS_FreeValue(data->context, ruby_data[1]);
|
|
4312
|
+
VALUE r_name;
|
|
4313
|
+
VALUE r_flags;
|
|
4314
|
+
VALUE r_block;
|
|
4315
|
+
rb_scan_args(argc, argv, "10*&", &r_name, &r_flags, &r_block);
|
|
1897
4316
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
4317
|
+
VMData *data;
|
|
4318
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
4319
|
+
|
|
4320
|
+
check_disposed(data);
|
|
4321
|
+
check_no_gvl_release_in_flight(data);
|
|
4322
|
+
|
|
4323
|
+
struct define_function_call call = {
|
|
4324
|
+
.data = data,
|
|
4325
|
+
.r_name = r_name,
|
|
4326
|
+
.r_flags = r_flags,
|
|
4327
|
+
.r_block = r_block,
|
|
4328
|
+
};
|
|
4329
|
+
return run_held_js_entry(data, define_global_function_body, (VALUE)&call);
|
|
1904
4330
|
}
|
|
1905
4331
|
|
|
1906
4332
|
struct js_entry_call
|
|
@@ -1910,14 +4336,79 @@ struct js_entry_call
|
|
|
1910
4336
|
VMData *data;
|
|
1911
4337
|
};
|
|
1912
4338
|
|
|
4339
|
+
// The converted arguments of a call. Owned by an ensure from before the first
|
|
4340
|
+
// conversion, because to_js_value runs inspect on the caller's own objects and
|
|
4341
|
+
// any one of them can raise part-way through — and the ones already converted
|
|
4342
|
+
// used to go with it, along with the array.
|
|
4343
|
+
struct js_call_args
|
|
4344
|
+
{
|
|
4345
|
+
JSContext *ctx;
|
|
4346
|
+
JSValue *j_args;
|
|
4347
|
+
int nargs;
|
|
4348
|
+
};
|
|
4349
|
+
|
|
4350
|
+
static VALUE js_call_args_release(VALUE p)
|
|
4351
|
+
{
|
|
4352
|
+
struct js_call_args *args = (struct js_call_args *)p;
|
|
4353
|
+
for (int i = 0; i < args->nargs; i++)
|
|
4354
|
+
JS_FreeValue(args->ctx, args->j_args[i]);
|
|
4355
|
+
xfree(args->j_args);
|
|
4356
|
+
return Qnil;
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
struct js_call_run
|
|
4360
|
+
{
|
|
4361
|
+
struct js_entry_call *call;
|
|
4362
|
+
struct js_call_args *args;
|
|
4363
|
+
};
|
|
4364
|
+
|
|
4365
|
+
static VALUE call_global_function_run(VALUE p);
|
|
4366
|
+
|
|
1913
4367
|
static VALUE call_global_function_body(VALUE p)
|
|
1914
4368
|
{
|
|
1915
4369
|
struct js_entry_call *call = (struct js_entry_call *)p;
|
|
1916
|
-
|
|
4370
|
+
struct js_call_args args = {call->data->context, NULL, call->argc - 1};
|
|
4371
|
+
if (args.nargs > 0)
|
|
4372
|
+
{
|
|
4373
|
+
args.j_args = xmalloc2(args.nargs, sizeof(JSValue));
|
|
4374
|
+
for (int i = 0; i < args.nargs; i++)
|
|
4375
|
+
args.j_args[i] = JS_UNDEFINED;
|
|
4376
|
+
}
|
|
4377
|
+
struct js_call_run run = {call, &args};
|
|
4378
|
+
return rb_ensure(call_global_function_run, (VALUE)&run, js_call_args_release, (VALUE)&args);
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
static VALUE call_global_function_run(VALUE p)
|
|
4382
|
+
{
|
|
4383
|
+
struct js_call_run *run = (struct js_call_run *)p;
|
|
4384
|
+
struct js_entry_call *call = run->call;
|
|
4385
|
+
struct js_call_args *args = run->args;
|
|
1917
4386
|
VALUE *argv = call->argv;
|
|
1918
4387
|
VMData *data = call->data;
|
|
1919
4388
|
VALUE r_name = argv[0];
|
|
1920
4389
|
|
|
4390
|
+
// Converted first, under a clock of its own. Mostly this is Ruby work —
|
|
4391
|
+
// inspect on the caller's objects, allocation, a GVL yield to another thread
|
|
4392
|
+
// — and none of it is the guest's to pay for, which is why the arm above the
|
|
4393
|
+
// resolution below starts the budget over. But a conversion can still run JS:
|
|
4394
|
+
// a File argument calls the proxy creator, which polls the interrupt handler
|
|
4395
|
+
// on the way, so without an arm here it ran on whatever clock the previous
|
|
4396
|
+
// entry left — a lapsed one interrupts the conversion at random and hands the
|
|
4397
|
+
// function a JS_EXCEPTION for an argument. Two arms, but not the two budgets
|
|
4398
|
+
// the previous commit had: no guest-written JS runs between them unless the
|
|
4399
|
+
// guest has replaced Proxy, and then it is bounded rather than unbounded.
|
|
4400
|
+
// (A Bignum used to call Number() here too; it converts without asking the
|
|
4401
|
+
// guest for anything now.)
|
|
4402
|
+
arm_eval_timer(data);
|
|
4403
|
+
for (int i = 0; i < args->nargs; i++)
|
|
4404
|
+
{
|
|
4405
|
+
args->j_args[i] = to_js_value(data->context, argv[i + 1]);
|
|
4406
|
+
// An argument that could not be built is not handed to the call as the
|
|
4407
|
+
// sentinel. The ensure that owns j_args releases what was built already.
|
|
4408
|
+
if (JS_IsException(args->j_args[i]))
|
|
4409
|
+
raise_js_exception(data->context); // raises
|
|
4410
|
+
}
|
|
4411
|
+
|
|
1921
4412
|
JSValue j_this = JS_UNDEFINED;
|
|
1922
4413
|
JSValue j_func;
|
|
1923
4414
|
|
|
@@ -1965,6 +4456,17 @@ static VALUE call_global_function_body(VALUE p)
|
|
|
1965
4456
|
rb_raise(rb_eTypeError, "function's name should be a Symbol or a String");
|
|
1966
4457
|
}
|
|
1967
4458
|
|
|
4459
|
+
// Armed once, above the resolution, for the reason define_global_function_body
|
|
4460
|
+
// and vm_m_import give: resolving the path runs JS — the JS_Eval of the first
|
|
4461
|
+
// segment, then a property read per segment, any of which can be a getter —
|
|
4462
|
+
// and it ran under whatever the previous entry point left behind. A lapsed
|
|
4463
|
+
// clock interrupted it spuriously; none at all, after a polyfill load's
|
|
4464
|
+
// disarm, left it unbounded. Once, because resolution and the call are one
|
|
4465
|
+
// budget: a second arm beside the JS_Call handed a single call twice
|
|
4466
|
+
// timeout_msec of guest JS, half in a getter on the path and half in the
|
|
4467
|
+
// function it found.
|
|
4468
|
+
arm_eval_timer(data);
|
|
4469
|
+
|
|
1968
4470
|
{
|
|
1969
4471
|
long path_len = RARRAY_LEN(r_path);
|
|
1970
4472
|
|
|
@@ -2017,31 +4519,13 @@ static VALUE call_global_function_body(VALUE p)
|
|
|
2017
4519
|
return Qnil;
|
|
2018
4520
|
}
|
|
2019
4521
|
|
|
2020
|
-
|
|
2021
|
-
JSValue *j_args = NULL;
|
|
2022
|
-
if (nargs > 0)
|
|
2023
|
-
{
|
|
2024
|
-
j_args = (JSValue *)malloc(sizeof(JSValue) * nargs);
|
|
2025
|
-
for (int i = 0; i < nargs; i++)
|
|
2026
|
-
j_args[i] = to_js_value(data->context, argv[i + 1]);
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
clock_gettime(CLOCK_MONOTONIC, &data->eval_time->started_at);
|
|
2030
|
-
JS_SetInterruptHandler(JS_GetRuntime(data->context), interrupt_handler, data->eval_time);
|
|
2031
|
-
|
|
2032
|
-
JSValue j_result = JS_Call(data->context, j_func, j_this, nargs, (JSValueConst *)j_args);
|
|
4522
|
+
JSValue j_result = JS_Call(data->context, j_func, j_this, args->nargs, (JSValueConst *)args->j_args);
|
|
2033
4523
|
|
|
2034
4524
|
JS_FreeValue(data->context, j_func);
|
|
2035
4525
|
JS_FreeValue(data->context, j_this);
|
|
2036
|
-
if (j_args)
|
|
2037
|
-
{
|
|
2038
|
-
for (int i = 0; i < nargs; i++)
|
|
2039
|
-
JS_FreeValue(data->context, j_args[i]);
|
|
2040
|
-
free(j_args);
|
|
2041
|
-
}
|
|
2042
4526
|
|
|
2043
4527
|
// js_std_await handles both async (promise) and sync results; frees j_result
|
|
2044
|
-
return to_rb_return_value(data->context,
|
|
4528
|
+
return to_rb_return_value(data->context, quickjsrb_host_await(data->context, j_result));
|
|
2045
4529
|
}
|
|
2046
4530
|
|
|
2047
4531
|
static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
@@ -2053,7 +4537,8 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
2053
4537
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
2054
4538
|
|
|
2055
4539
|
check_disposed(data);
|
|
2056
|
-
|
|
4540
|
+
check_vm_poisoned(data);
|
|
4541
|
+
check_js_entry_owner(data);
|
|
2057
4542
|
|
|
2058
4543
|
// evals_in_flight stays elevated for the whole call, not just the
|
|
2059
4544
|
// JS_Eval / JS_Call moments: the body holds live JSValues across Ruby
|
|
@@ -2061,7 +4546,7 @@ static VALUE vm_m_callGlobalFunction(int argc, VALUE *argv, VALUE r_self)
|
|
|
2061
4546
|
// conversion), and a concurrent dispose! landing in such a gap would
|
|
2062
4547
|
// free the runtime out from under them.
|
|
2063
4548
|
struct js_entry_call call = {argc, argv, data};
|
|
2064
|
-
return
|
|
4549
|
+
return run_held_js_checkpoint_entry(data, call_global_function_body, (VALUE)&call);
|
|
2065
4550
|
}
|
|
2066
4551
|
|
|
2067
4552
|
static VALUE vm_m_set_module_loader(VALUE r_self, VALUE r_loader)
|
|
@@ -2072,6 +4557,12 @@ static VALUE vm_m_set_module_loader(VALUE r_self, VALUE r_loader)
|
|
|
2072
4557
|
if (!NIL_P(r_loader) && !rb_obj_is_kind_of(r_loader, rb_cProc))
|
|
2073
4558
|
rb_raise(rb_eTypeError, "module_loader must be a Proc or nil");
|
|
2074
4559
|
|
|
4560
|
+
// register_module_loader_funcs below dereferences the context, so a
|
|
4561
|
+
// disposed VM was a use-after-free here, and a VM being evaluated by
|
|
4562
|
+
// another thread means swapping the loader out from under a running
|
|
4563
|
+
// import. Neither was checked.
|
|
4564
|
+
check_disposed(data);
|
|
4565
|
+
check_js_entry_owner(data);
|
|
2075
4566
|
check_no_gvl_release_in_flight(data);
|
|
2076
4567
|
data->module_loader = r_loader;
|
|
2077
4568
|
// Stale entries from the previous loader's policy would survive the
|
|
@@ -2125,14 +4616,22 @@ static VALUE import_body(VALUE p)
|
|
|
2125
4616
|
VALUE r_custom_exposure = rb_hash_aref(r_opts, ID2SYM(rb_intern("code_to_expose")));
|
|
2126
4617
|
|
|
2127
4618
|
char *filename;
|
|
4619
|
+
char generated_filename[QUICKJSRB_GENERATED_NAME_SIZE];
|
|
2128
4620
|
VALUE r_seeded_key = Qnil;
|
|
2129
4621
|
if (!NIL_P(r_filename))
|
|
2130
4622
|
{
|
|
4623
|
+
// Borrowed for the rest of the call, so r_filename has to outlive the
|
|
4624
|
+
// bridge snprintf below. A literal String is rooted by the kwargs hash
|
|
4625
|
+
// through argv, but StringValueCStr writes a coerced String back through
|
|
4626
|
+
// &r_filename, and a to_str result is referenced by nothing else — only
|
|
4627
|
+
// this local, which the compiler is free to treat as dead right here
|
|
4628
|
+
// while filename is still being read. Guarded after the last read.
|
|
2131
4629
|
filename = StringValueCStr(r_filename);
|
|
2132
4630
|
}
|
|
2133
4631
|
else
|
|
2134
4632
|
{
|
|
2135
|
-
|
|
4633
|
+
random_filename(generated_filename);
|
|
4634
|
+
filename = generated_filename;
|
|
2136
4635
|
char *source = StringValueCStr(r_from);
|
|
2137
4636
|
JSValue module = JS_Eval(data->context, source, strlen(source), filename, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
|
|
2138
4637
|
if (JS_IsException(module))
|
|
@@ -2179,6 +4678,7 @@ static VALUE import_body(VALUE p)
|
|
|
2179
4678
|
int length = snprintf(NULL, 0, importAndGlobalizeModule, import_name, filename, globalize);
|
|
2180
4679
|
char *result = (char *)malloc(length + 1);
|
|
2181
4680
|
snprintf(result, length + 1, importAndGlobalizeModule, import_name, filename, globalize);
|
|
4681
|
+
RB_GC_GUARD(r_filename);
|
|
2182
4682
|
|
|
2183
4683
|
JSValue j_codeResult = JS_Eval(data->context, result, strlen(result), vmInternalFilename, JS_EVAL_TYPE_MODULE);
|
|
2184
4684
|
free(result);
|
|
@@ -2188,7 +4688,7 @@ static VALUE import_body(VALUE p)
|
|
|
2188
4688
|
// Module eval returns a Promise. Awaiting it surfaces top-level throws,
|
|
2189
4689
|
// rejected dynamic imports, and rejected top-level awaits as Ruby
|
|
2190
4690
|
// exceptions instead of silently dropping them.
|
|
2191
|
-
JSValue j_awaited =
|
|
4691
|
+
JSValue j_awaited = quickjsrb_host_await(data->context, j_codeResult);
|
|
2192
4692
|
if (JS_IsException(j_awaited))
|
|
2193
4693
|
return to_rb_value(data->context, j_awaited);
|
|
2194
4694
|
JS_FreeValue(data->context, j_awaited);
|
|
@@ -2205,7 +4705,8 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
2205
4705
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
2206
4706
|
|
|
2207
4707
|
check_disposed(data);
|
|
2208
|
-
|
|
4708
|
+
check_vm_poisoned(data);
|
|
4709
|
+
check_js_entry_owner(data);
|
|
2209
4710
|
|
|
2210
4711
|
// Module top-level code is user JS like any eval — budget it. Without
|
|
2211
4712
|
// this, import ran under whatever handler the previous entry point
|
|
@@ -2219,13 +4720,14 @@ static VALUE vm_m_import(int argc, VALUE *argv, VALUE r_self)
|
|
|
2219
4720
|
// evals_in_flight elevated for the whole call so a concurrent dispose!
|
|
2220
4721
|
// can't free the runtime in those gaps.
|
|
2221
4722
|
struct js_entry_call call = {argc, argv, data};
|
|
2222
|
-
return
|
|
4723
|
+
return run_held_js_checkpoint_entry(data, import_body, (VALUE)&call);
|
|
2223
4724
|
}
|
|
2224
4725
|
|
|
2225
4726
|
RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
|
|
2226
4727
|
{
|
|
2227
4728
|
rb_require("json");
|
|
2228
4729
|
rb_require("securerandom");
|
|
4730
|
+
quickjsrb_init_handle_source();
|
|
2229
4731
|
|
|
2230
4732
|
VALUE r_module_quickjs = rb_define_module("Quickjs");
|
|
2231
4733
|
r_define_constants(r_module_quickjs);
|
|
@@ -2236,6 +4738,9 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
|
|
|
2236
4738
|
rb_define_method(r_class_vm, "initialize", vm_m_initialize, -1);
|
|
2237
4739
|
rb_define_method(r_class_vm, "eval_code", vm_m_evalCode, -1);
|
|
2238
4740
|
rb_define_private_method(r_class_vm, "_compile_to_bytecode", vm_m_compile, -1);
|
|
4741
|
+
rb_define_private_method(r_class_vm, "_compile_module_to_bytecode", vm_m_compileModule, 2);
|
|
4742
|
+
rb_define_private_method(r_class_vm, "_preload_module_bytecode", vm_m_preloadModuleBytecode, 2);
|
|
4743
|
+
rb_define_private_method(r_class_vm, "_pending_module_source_count", vm_m_pendingModuleSourceCount, 0);
|
|
2239
4744
|
rb_define_private_method(r_class_vm, "_run_bytecode", vm_m_evalBytecode, 1);
|
|
2240
4745
|
rb_define_private_method(r_class_vm, "_load_polyfill_bytecode", vm_m_loadPolyfillBytecode, 1);
|
|
2241
4746
|
rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
|
|
@@ -2248,6 +4753,7 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
|
|
|
2248
4753
|
rb_define_method(r_class_vm, "memory_usage", vm_m_memoryUsage, 0);
|
|
2249
4754
|
rb_define_method(r_class_vm, "gc!", vm_m_runGC, 0);
|
|
2250
4755
|
rb_define_method(r_class_vm, "memory_poisoned?", vm_m_memoryPoisoned, 0);
|
|
4756
|
+
rb_define_method(r_class_vm, "poisoned?", vm_m_poisoned, 0);
|
|
2251
4757
|
rb_define_method(r_class_vm, "dispose!", vm_m_dispose, 0);
|
|
2252
4758
|
rb_define_method(r_class_vm, "disposed?", vm_m_disposed, 0);
|
|
2253
4759
|
rb_define_method(r_class_vm, "drain_jobs!", vm_m_drainJobs, 0);
|
|
@@ -2259,6 +4765,7 @@ static VALUE vm_m_memoryUsage(VALUE r_self)
|
|
|
2259
4765
|
VMData *data;
|
|
2260
4766
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
2261
4767
|
check_disposed(data);
|
|
4768
|
+
check_js_entry_owner(data);
|
|
2262
4769
|
JSMemoryUsage s;
|
|
2263
4770
|
JS_ComputeMemoryUsage(JS_GetRuntime(data->context), &s);
|
|
2264
4771
|
VALUE h = rb_hash_new();
|
|
@@ -2282,6 +4789,7 @@ static VALUE vm_m_runGC(VALUE r_self)
|
|
|
2282
4789
|
VMData *data;
|
|
2283
4790
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
2284
4791
|
check_disposed(data);
|
|
4792
|
+
check_js_entry_owner(data);
|
|
2285
4793
|
JS_RunGC(JS_GetRuntime(data->context));
|
|
2286
4794
|
return Qnil;
|
|
2287
4795
|
}
|
|
@@ -2291,14 +4799,18 @@ static VALUE drain_jobs_body(VALUE p)
|
|
|
2291
4799
|
VMData *data = (VMData *)p;
|
|
2292
4800
|
JSRuntime *runtime = JS_GetRuntime(data->context);
|
|
2293
4801
|
int executed = 0;
|
|
2294
|
-
|
|
4802
|
+
if (JS_IsJobPending(runtime))
|
|
2295
4803
|
{
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
4804
|
+
arm_eval_timer(data);
|
|
4805
|
+
for (;;)
|
|
4806
|
+
{
|
|
4807
|
+
int err = JS_ExecutePendingJob(runtime, NULL);
|
|
4808
|
+
if (err == 0)
|
|
4809
|
+
break;
|
|
4810
|
+
if (err < 0)
|
|
4811
|
+
return to_rb_value(data->context, JS_EXCEPTION); // raises
|
|
4812
|
+
executed++;
|
|
4813
|
+
}
|
|
2302
4814
|
}
|
|
2303
4815
|
return INT2NUM(executed);
|
|
2304
4816
|
}
|
|
@@ -2309,14 +4821,15 @@ static VALUE vm_m_drainJobs(VALUE r_self)
|
|
|
2309
4821
|
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
2310
4822
|
|
|
2311
4823
|
check_disposed(data);
|
|
2312
|
-
|
|
4824
|
+
check_vm_poisoned(data);
|
|
4825
|
+
check_js_entry_owner(data);
|
|
2313
4826
|
|
|
2314
|
-
|
|
4827
|
+
// Rejections can be pending with no job queued.
|
|
4828
|
+
if (!JS_IsJobPending(JS_GetRuntime(data->context)) &&
|
|
4829
|
+
data->pending_rejections.live == 0)
|
|
2315
4830
|
return INT2NUM(0);
|
|
2316
4831
|
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
return run_held_js_entry(data, drain_jobs_body, (VALUE)data);
|
|
4832
|
+
return run_held_js_checkpoint_entry(data, drain_jobs_body, (VALUE)data);
|
|
2320
4833
|
}
|
|
2321
4834
|
|
|
2322
4835
|
static VALUE vm_m_memoryPoisoned(VALUE r_self)
|
|
@@ -2326,18 +4839,49 @@ static VALUE vm_m_memoryPoisoned(VALUE r_self)
|
|
|
2326
4839
|
return data->oom_poisoned ? Qtrue : Qfalse;
|
|
2327
4840
|
}
|
|
2328
4841
|
|
|
4842
|
+
// Answers for either latch, where memory_poisoned? answers only for its own.
|
|
4843
|
+
// A VM whose handle source broke refuses every entry point while
|
|
4844
|
+
// memory_poisoned? says false, which leaves a host following the README's
|
|
4845
|
+
// recycle pattern holding one that looks healthy and does nothing. The two are
|
|
4846
|
+
// worth telling apart: recreating the VM clears an out-of-memory, and does not
|
|
4847
|
+
// clear a SecureRandom that is still returning nil.
|
|
4848
|
+
static VALUE vm_m_poisoned(VALUE r_self)
|
|
4849
|
+
{
|
|
4850
|
+
VMData *data;
|
|
4851
|
+
TypedData_Get_Struct(r_self, VMData, &vm_type, data);
|
|
4852
|
+
// Only the latches, not the held exception. That one is handed back once and
|
|
4853
|
+
// gone, and the VM is usable after it, so answering true for it would tell a
|
|
4854
|
+
// host following the README's recycle path to throw away a healthy VM and go
|
|
4855
|
+
// looking for a SecureRandom stub that does not exist.
|
|
4856
|
+
return (data->oom_poisoned || data->handle_source_broken) ? Qtrue : Qfalse;
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
|
|
2329
4860
|
// JS_FreeContext + JS_FreeRuntime walk the entire heap to run finalisers.
|
|
2330
4861
|
// On a VM with polyfills loaded this can be tens of milliseconds — run it
|
|
2331
4862
|
// without the GVL so other Ruby threads (e.g. the next pool builder) keep
|
|
2332
4863
|
// progressing. Safe to release because nothing in the teardown path calls
|
|
2333
4864
|
// back into Ruby: module_loader, console, and define_function callbacks
|
|
2334
4865
|
// only fire during JS execution, not during free.
|
|
4866
|
+
struct teardown_job
|
|
4867
|
+
{
|
|
4868
|
+
JSContext *context;
|
|
4869
|
+
bool std_handlers_installed;
|
|
4870
|
+
};
|
|
4871
|
+
|
|
2335
4872
|
static void *vm_dispose_no_gvl(void *p)
|
|
2336
4873
|
{
|
|
2337
|
-
|
|
4874
|
+
struct teardown_job *job = (struct teardown_job *)p;
|
|
4875
|
+
vm_teardown_context(job->context, job->std_handlers_installed);
|
|
2338
4876
|
return NULL;
|
|
2339
4877
|
}
|
|
2340
4878
|
|
|
4879
|
+
static VALUE dispose_notify_body(VALUE p)
|
|
4880
|
+
{
|
|
4881
|
+
quickjsrb_notify_all_rejections((VMData *)p, false);
|
|
4882
|
+
return Qnil;
|
|
4883
|
+
}
|
|
4884
|
+
|
|
2341
4885
|
static VALUE vm_m_dispose(VALUE r_self)
|
|
2342
4886
|
{
|
|
2343
4887
|
VMData *data;
|
|
@@ -2355,23 +4899,36 @@ static VALUE vm_m_dispose(VALUE r_self)
|
|
|
2355
4899
|
if (data->evals_in_flight > 0)
|
|
2356
4900
|
rb_raise(rb_eThreadError, "cannot dispose a Quickjs::VM while it is evaluating");
|
|
2357
4901
|
|
|
4902
|
+
// Jobs still queued will never run, so what is pending is unhandled. Counted
|
|
4903
|
+
// as an entry, so the handler cannot dispose! under it.
|
|
4904
|
+
if (data->pending_rejections.live > 0 && !data->oom_poisoned)
|
|
4905
|
+
run_held_js_entry(data, dispose_notify_body, (VALUE)data);
|
|
4906
|
+
rejection_list_free(data->context, &data->pending_rejections);
|
|
4907
|
+
|
|
2358
4908
|
if (!JS_IsUndefined(data->j_file_proxy_creator))
|
|
2359
4909
|
{
|
|
2360
4910
|
JS_FreeValue(data->context, data->j_file_proxy_creator);
|
|
2361
4911
|
data->j_file_proxy_creator = JS_UNDEFINED;
|
|
2362
4912
|
}
|
|
4913
|
+
if (!JS_IsUndefined(data->j_blob_ctor))
|
|
4914
|
+
{
|
|
4915
|
+
JS_FreeValue(data->context, data->j_blob_ctor);
|
|
4916
|
+
data->j_blob_ctor = JS_UNDEFINED;
|
|
4917
|
+
}
|
|
2363
4918
|
|
|
2364
4919
|
// Mark disposed before releasing the GVL so a concurrent dfree finds
|
|
2365
4920
|
// disposed=true and skips its own teardown.
|
|
2366
4921
|
data->disposed = true;
|
|
2367
4922
|
|
|
2368
|
-
|
|
4923
|
+
struct teardown_job job = {data->context, data->std_handlers_installed};
|
|
4924
|
+
rb_thread_call_without_gvl(vm_dispose_no_gvl, &job, NULL, NULL);
|
|
2369
4925
|
|
|
2370
4926
|
// Drop references to user-supplied closures so Ruby GC can reclaim them
|
|
2371
4927
|
// (and anything they captured) before the wrapping VM object itself is
|
|
2372
4928
|
// collected. Matters for pool-rebuild workloads that dispose eagerly.
|
|
2373
4929
|
data->defined_functions = rb_hash_new();
|
|
2374
4930
|
data->alive_objects = rb_hash_new();
|
|
4931
|
+
data->alive_handles = rb_hash_new();
|
|
2375
4932
|
data->log_listener = Qnil;
|
|
2376
4933
|
data->module_loader = Qnil;
|
|
2377
4934
|
|