rusty_racer 0.2.7 → 0.2.8
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/CHANGELOG.md +33 -0
- data/README.md +56 -0
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +165 -15
- data/ext/rusty_racer/src/marshal.rs +76 -1
- data/ext/rusty_racer/src/ops.rs +28 -20
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +26 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6eedc8d154897325a2990e5ed6999a368b7c81c64528d5d406ed469e5d808fc4
|
|
4
|
+
data.tar.gz: f0c356c5b22a4574323e7efe73c56bd07267d5428438d22dadf2c4a08953b468
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02acd6e295f14544a9f146f0b916ac1ac585b63634bb12c353d39059dc7ffef50337e76cd81c318463060c2e8f901a19667f384cbd1967ed776290dfd1d7c0cb
|
|
7
|
+
data.tar.gz: bc2513ed51e0a7ed2c45bc4c8cc85a051a7d83f9d1cdc56675cc2e1a3fcfd915c7b77bede14f91ac10e475d877e43563cea38c734d5cab36a2be6b806200312c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.8
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- A **host object** per realm: an object with no prototype that no global
|
|
8
|
+
holds (its global keeps it under a V8 private key), for the host and the
|
|
9
|
+
realm's own bindings to share where a page's script can neither read, call nor
|
|
10
|
+
replace what they put there. Every path the host names starts there when it
|
|
11
|
+
begins with `@`: `Context#attach('@fetch', proc)` / `#attach_many` put a host
|
|
12
|
+
function there (intermediates with no prototype, every property defined, not
|
|
13
|
+
assigned), `#function('@steps.query')` / `#call` / `#call_void` find one
|
|
14
|
+
there, `Module#evaluate(report: '@reportError')` reports to one there, and
|
|
15
|
+
`Isolate.new(host_namespace: '@RustyRacer')` installs rusty_racer's own
|
|
16
|
+
functions (`contextGlobal`, `contextOf`, `drainMicrotasks`, …) there instead
|
|
17
|
+
of on the global. `RustyRacer::HOST_OBJECT`, as an argument (or a host
|
|
18
|
+
function's result) crossing into JS, is the realm's host object — how the
|
|
19
|
+
bindings are handed it, and the realm's host object crosses back into Ruby as
|
|
20
|
+
`HOST_OBJECT`; an embedder linking rusty_racer as a library reaches it
|
|
21
|
+
natively with `rusty_racer::host_object(scope)`, from its realm init hook.
|
|
22
|
+
An `@` path is walked through own properties alone, so nothing a page put on
|
|
23
|
+
a prototype sees or answers the walk. A namespace on the host object has
|
|
24
|
+
`hostObjectOf(id)` besides: another realm's host object, for one realm's
|
|
25
|
+
bindings to reach another's. Made as a realm first uses it, made anew with the
|
|
26
|
+
realm by `reset`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- A path beginning with `@` no longer names a property of the global whose name
|
|
31
|
+
begins with `@` (`globalThis["@x"]`): it names the host object's.
|
|
32
|
+
- `Module#evaluate(report:)` looks the reporter up as before — one property,
|
|
33
|
+
dots and all — on the global, or, for an `@` path, as `Context#function` finds
|
|
34
|
+
it on the host object.
|
|
35
|
+
|
|
3
36
|
## 0.2.7
|
|
4
37
|
|
|
5
38
|
### Changed
|
data/README.md
CHANGED
|
@@ -123,6 +123,62 @@ realm (`frames[0].fn` in an embedder whose realms see each other) holds that
|
|
|
123
123
|
realm's function for as long as the handle's own realm lives, as any reference
|
|
124
124
|
kept there would.
|
|
125
125
|
|
|
126
|
+
### The host object
|
|
127
|
+
|
|
128
|
+
An embedder that runs a page's script beside its own bindings wants the host's
|
|
129
|
+
functions where the bindings can call them and the page can't. Every realm has a
|
|
130
|
+
**host object** for that: an object of the realm with no prototype, which no
|
|
131
|
+
global holds — the global keeps it under a V8 private key, which no script can
|
|
132
|
+
name. A path the host names that begins with `@` starts there instead of at
|
|
133
|
+
`globalThis`:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# rusty_racer's own functions (contextGlobal, contextOf, drainMicrotasks, …)
|
|
137
|
+
# there too, rather than on a global
|
|
138
|
+
iso = RustyRacer::Isolate.new(host_namespace: "@RustyRacer")
|
|
139
|
+
ctx = iso.context
|
|
140
|
+
|
|
141
|
+
# Host functions the page never sees: no global, nothing to read, call or replace.
|
|
142
|
+
ctx.attach("@fetch", ->(url) { fetch(url) })
|
|
143
|
+
ctx.attach_many("@net.open" => open_proc, "@net.close" => close_proc)
|
|
144
|
+
|
|
145
|
+
# The bindings are handed the object as an argument — HOST_OBJECT crosses into
|
|
146
|
+
# JS as the realm's host object — and keep it in a closure or a module binding.
|
|
147
|
+
ctx.call("installBindings", RustyRacer::HOST_OBJECT)
|
|
148
|
+
|
|
149
|
+
# What they put there, the host calls by its path, with no global name.
|
|
150
|
+
query = ctx.function("@steps.query") # host.steps.query, `this` = host.steps
|
|
151
|
+
query.call("#main")
|
|
152
|
+
ctx.call("@steps.flush")
|
|
153
|
+
mod.evaluate(report: "@reportError") # Module#evaluate's reporter, too
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
An `@` path is walked through **own** properties alone: the intermediates an
|
|
157
|
+
`@` attach makes (`@net` above) have no prototype either, every property it
|
|
158
|
+
makes is *defined*, and a segment an object lacks reads as missing rather than
|
|
159
|
+
off its prototype — so no accessor a page put on `Object.prototype` or
|
|
160
|
+
`Function.prototype` sees the walk, takes the host's function, or hands the host
|
|
161
|
+
one of its own. A `Module#evaluate(report:)` `@` path is walked the same way.
|
|
162
|
+
|
|
163
|
+
Which realm's: each realm has its own, made as it is first used, and made anew
|
|
164
|
+
with the realm by `reset` (what was attached there goes as a global's does).
|
|
165
|
+
`HOST_OBJECT` is the host object of the realm the operation runs in — a
|
|
166
|
+
Context's, a `Function` handle's (whatever realm the function it calls is of),
|
|
167
|
+
or, as a host function's result, the realm that host function was attached to —
|
|
168
|
+
and that realm's host object crosses back into Ruby as `HOST_OBJECT`; another
|
|
169
|
+
realm's reads as a plain object. With `host_namespace: "@…"` the namespace has
|
|
170
|
+
one member more, `hostObjectOf(id)`: realm `id`'s host object, for one realm's
|
|
171
|
+
bindings to reach another's (a namespace on the global never has it).
|
|
172
|
+
|
|
173
|
+
Hand it over before any script of the page's runs: `call` reaches the bindings
|
|
174
|
+
by a global name, which a page could otherwise have replaced — and have the
|
|
175
|
+
bindings delete that name as they take it. An embedder linking rusty_racer as a
|
|
176
|
+
library reaches it natively with `rusty_racer::host_object(scope)` instead —
|
|
177
|
+
from its realm init hook, say, to hand it to bindings baked into a snapshot
|
|
178
|
+
before any script runs. The object has no prototype, so V8 keeps it as a
|
|
179
|
+
dictionary: bindings that call through it in a hot loop do better to keep the
|
|
180
|
+
functions they read off it.
|
|
181
|
+
|
|
126
182
|
### Releasing handles
|
|
127
183
|
|
|
128
184
|
A `Function`, `Script` or `Module` handle is released when it is `#dispose`d —
|
data/ext/rusty_racer/Cargo.toml
CHANGED
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -1027,6 +1027,9 @@ struct V8State {
|
|
|
1027
1027
|
retiring: Vec<(v8::Global<v8::Context>, v8::UniqueRef<v8::MicrotaskQueue>)>,
|
|
1028
1028
|
next_context_id: i32,
|
|
1029
1029
|
host_namespace: Option<String>,
|
|
1030
|
+
// The private key every realm's global holds its host object under
|
|
1031
|
+
// (host_object_key), once something has asked for one.
|
|
1032
|
+
host_key: Option<v8::Global<v8::Private>>,
|
|
1030
1033
|
// One security token shared by every realm of this isolate: the
|
|
1031
1034
|
// embedder's frames are same-origin, so cross-context access (e.g.
|
|
1032
1035
|
// NS.contextGlobal) must pass V8's access checks, which compare the
|
|
@@ -1998,6 +2001,30 @@ fn context_global(
|
|
|
1998
2001
|
}
|
|
1999
2002
|
}
|
|
2000
2003
|
|
|
2004
|
+
// NS.hostObjectOf(id) -> the host object of context |id| (host_object), made
|
|
2005
|
+
// there if it has none yet — for one realm's bindings to reach another's, as
|
|
2006
|
+
// contextGlobal reaches its global. Only on a namespace that is itself on the
|
|
2007
|
+
// host object (`host_namespace: '@…'`): a namespace on the global would hand
|
|
2008
|
+
// every realm's host object to any script. Throws on an unknown/disposed id.
|
|
2009
|
+
fn host_object_of(
|
|
2010
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
2011
|
+
args: v8::FunctionCallbackArguments<'_>,
|
|
2012
|
+
mut rv: v8::ReturnValue<'_, v8::Value>,
|
|
2013
|
+
) {
|
|
2014
|
+
let Some(id) = args.get(0).int32_value(scope) else {
|
|
2015
|
+
throw_js_error(scope, "hostObjectOf expects a context id");
|
|
2016
|
+
return;
|
|
2017
|
+
};
|
|
2018
|
+
match context_for(istate!(scope), id) {
|
|
2019
|
+
Some(g) => {
|
|
2020
|
+
let context = v8::Local::new(scope, &g);
|
|
2021
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
2022
|
+
rv.set(host_object(scope).into());
|
|
2023
|
+
}
|
|
2024
|
+
None => throw_js_error(scope, &format!("unknown context {id}")),
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2001
2028
|
// NS.contextOf(value) -> the id of the context |value| was created in, or
|
|
2002
2029
|
// undefined for primitives and for objects whose context is no longer a live
|
|
2003
2030
|
// realm (e.g. it was reset away). Lets the embedder attribute a function or
|
|
@@ -2269,6 +2296,97 @@ pub fn set_realm_init_hook(hook: RealmInitHook) {
|
|
|
2269
2296
|
let _ = REALM_INIT_HOOK.set(hook);
|
|
2270
2297
|
}
|
|
2271
2298
|
|
|
2299
|
+
// The HOST OBJECT of the realm |scope| has entered: an object of the realm's,
|
|
2300
|
+
// with no prototype, that no property of any global holds — the realm's global
|
|
2301
|
+
// keeps it under a private key, which no script can name — so the host and the
|
|
2302
|
+
// realm's own bindings share it and a page's script neither reads, calls nor
|
|
2303
|
+
// replaces what it holds. A path that begins `@` starts from it rather than from
|
|
2304
|
+
// the global: `attach('@x.y', proc)` puts a host function there,
|
|
2305
|
+
// `Context#function('@x.y')` / `#call('@x.y')` find one there, `host_namespace:
|
|
2306
|
+
// '@NS'` installs the namespace there, `evaluate(report: '@r')` reports to one
|
|
2307
|
+
// there; and `RustyRacer::HOST_OBJECT`, as an argument crossing into JS, is this
|
|
2308
|
+
// object — how the bindings are handed it, where an embedder's realm init hook
|
|
2309
|
+
// (which calls this) does not hand it over itself. Made as it is first asked
|
|
2310
|
+
// for, so a realm that never uses it carries none.
|
|
2311
|
+
//
|
|
2312
|
+
// The private key lives with the global, so the object goes with its realm: a
|
|
2313
|
+
// reset or a disposal needs nothing of it, and no context slot is used (see
|
|
2314
|
+
// id_of_context for why realms keep none).
|
|
2315
|
+
pub fn host_object<'s>(scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> {
|
|
2316
|
+
if let Some(object) = existing_host_object(scope) {
|
|
2317
|
+
return object;
|
|
2318
|
+
}
|
|
2319
|
+
let object = null_prototype_object(scope);
|
|
2320
|
+
let global = scope.get_current_context().global(scope);
|
|
2321
|
+
let key = host_object_key(scope);
|
|
2322
|
+
global.set_private(scope, key, object.into());
|
|
2323
|
+
object
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// The current realm's host object if it has made one yet, without making it.
|
|
2327
|
+
pub(crate) fn existing_host_object<'s>(
|
|
2328
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
2329
|
+
) -> Option<v8::Local<'s, v8::Object>> {
|
|
2330
|
+
let global = scope.get_current_context().global(scope);
|
|
2331
|
+
let key = host_object_key(scope);
|
|
2332
|
+
v8::Local::<v8::Object>::try_from(global.get_private(scope, key)?).ok()
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// The private key a global holds its host object under: an API private, so it
|
|
2336
|
+
// is one key per isolate whoever asks — the binding's own isolates keep it
|
|
2337
|
+
// (V8State::host_key) rather than looking it up by name at every use.
|
|
2338
|
+
fn host_object_key<'s>(scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Private> {
|
|
2339
|
+
let kept = scope
|
|
2340
|
+
.get_slot::<IsolateState>()
|
|
2341
|
+
.and_then(|state| state.realms.host_key.clone());
|
|
2342
|
+
if let Some(key) = kept {
|
|
2343
|
+
return v8::Local::new(scope, &key);
|
|
2344
|
+
}
|
|
2345
|
+
let name = v8::String::new(scope, "rusty_racer:host_object").expect("a short string");
|
|
2346
|
+
let key = v8::Private::for_api(scope, Some(name));
|
|
2347
|
+
let kept = v8::Global::new(scope, key);
|
|
2348
|
+
if let Some(state) = scope.get_slot_mut::<IsolateState>() {
|
|
2349
|
+
state.realms.host_key = Some(kept);
|
|
2350
|
+
}
|
|
2351
|
+
key
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// An object of the current realm with no prototype: a read of a name it lacks
|
|
2355
|
+
// ends there, never in an Object.prototype a page may have put a getter on.
|
|
2356
|
+
fn null_prototype_object<'s>(scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> {
|
|
2357
|
+
let null = v8::null(scope).into();
|
|
2358
|
+
v8::Object::with_prototype_and_properties(scope, null, &[], &[])
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// Where a path starts, and the rest of it: `@a.b` is the host object's `a.b`, any
|
|
2362
|
+
// other path the global's.
|
|
2363
|
+
pub(crate) fn path_root<'s, 'p>(
|
|
2364
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
2365
|
+
path: &'p str,
|
|
2366
|
+
) -> (v8::Local<'s, v8::Object>, &'p str, bool) {
|
|
2367
|
+
match path.strip_prefix('@') {
|
|
2368
|
+
Some(rest) => (host_object(scope), rest, true),
|
|
2369
|
+
None => (scope.get_current_context().global(scope), path, false),
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// One segment of a path read off |holder|: an ordinary [[Get]] on the global's
|
|
2374
|
+
// side; on the host object's (|hidden|), its OWN property alone — undefined
|
|
2375
|
+
// where it has none, so no prototype of an object along the way (Object's, a
|
|
2376
|
+
// function's) is ever consulted, and a getter a page put there neither sees
|
|
2377
|
+
// the walk nor hands it an object of the page's. None when the read threw.
|
|
2378
|
+
pub(crate) fn path_segment<'s>(
|
|
2379
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
2380
|
+
holder: v8::Local<v8::Object>,
|
|
2381
|
+
key: v8::Local<v8::String>,
|
|
2382
|
+
hidden: bool,
|
|
2383
|
+
) -> Option<v8::Local<'s, v8::Value>> {
|
|
2384
|
+
if hidden && !holder.has_own_property(scope, key.into())? {
|
|
2385
|
+
return Some(v8::undefined(scope).into());
|
|
2386
|
+
}
|
|
2387
|
+
holder.get(scope, key.into())
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2272
2390
|
fn new_realm(
|
|
2273
2391
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
2274
2392
|
context_id: i32,
|
|
@@ -2373,9 +2491,12 @@ fn flush_retiring(scope: &mut v8::PinScope<'_, '_, ()>) {
|
|
|
2373
2491
|
drop(retiring);
|
|
2374
2492
|
}
|
|
2375
2493
|
|
|
2376
|
-
// Inject globalThis.<name> = { drainMicrotasks } into a context
|
|
2377
|
-
//
|
|
2378
|
-
//
|
|
2494
|
+
// Inject globalThis.<name> = { drainMicrotasks, ... } into a context — or, for
|
|
2495
|
+
// an `@name`, put it on the realm's host object instead, with no prototype, where
|
|
2496
|
+
// only the host and the realm's bindings reach it, and with one member more
|
|
2497
|
+
// there: hostObjectOf(id), another realm's host object. Re-run on reset_realm so the
|
|
2498
|
+
// fresh realm keeps the namespace. Takes a scope (not the isolate) so
|
|
2499
|
+
// service_request can call it from nested servicing too.
|
|
2379
2500
|
fn install_host_namespace(
|
|
2380
2501
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
2381
2502
|
ctx: &v8::Global<v8::Context>,
|
|
@@ -2384,7 +2505,12 @@ fn install_host_namespace(
|
|
|
2384
2505
|
v8::scope!(let scope, &mut *scope);
|
|
2385
2506
|
let context = v8::Local::new(scope, ctx);
|
|
2386
2507
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
2387
|
-
let
|
|
2508
|
+
let (holder, name, hidden) = path_root(scope, name);
|
|
2509
|
+
let ns = if hidden {
|
|
2510
|
+
null_prototype_object(scope)
|
|
2511
|
+
} else {
|
|
2512
|
+
v8::Object::new(scope)
|
|
2513
|
+
};
|
|
2388
2514
|
let members: [(&str, Option<v8::Local<v8::Function>>); 9] = [
|
|
2389
2515
|
(
|
|
2390
2516
|
"drainMicrotasks",
|
|
@@ -2402,40 +2528,58 @@ fn install_host_namespace(
|
|
|
2402
2528
|
("shareIn", v8::Function::new(scope, share_in)),
|
|
2403
2529
|
("transferDrop", v8::Function::new(scope, transfer_drop)),
|
|
2404
2530
|
];
|
|
2405
|
-
|
|
2531
|
+
let hidden_members: &[(&str, Option<v8::Local<v8::Function>>)] = if hidden {
|
|
2532
|
+
&[("hostObjectOf", v8::Function::new(scope, host_object_of))]
|
|
2533
|
+
} else {
|
|
2534
|
+
&[]
|
|
2535
|
+
};
|
|
2536
|
+
for (member, function) in members.into_iter().chain(hidden_members.iter().copied()) {
|
|
2406
2537
|
if let (Some(f), Some(k)) = (function, v8::String::new(scope, member)) {
|
|
2407
2538
|
ns.create_data_property(scope, k.into(), f.into());
|
|
2408
2539
|
}
|
|
2409
2540
|
}
|
|
2410
2541
|
if let Some(key) = v8::String::new(scope, name) {
|
|
2411
|
-
|
|
2412
|
-
global.create_data_property(scope, key.into(), ns.into());
|
|
2542
|
+
holder.create_data_property(scope, key.into(), ns.into());
|
|
2413
2543
|
}
|
|
2414
2544
|
}
|
|
2415
2545
|
|
|
2416
2546
|
// Set |function| at the dotted property path |name| on the global, creating
|
|
2417
2547
|
// intermediate plain objects as needed — so attach("MiniRacer.foo", ...) lands
|
|
2418
2548
|
// under the namespace whether or not globalThis.MiniRacer already exists, while
|
|
2419
|
-
// a bare "foo" still attaches on the global.
|
|
2549
|
+
// a bare "foo" still attaches on the global. An `@` path lands on the realm's
|
|
2550
|
+
// host object instead: walked through own properties alone (path_segment), its
|
|
2551
|
+
// missing intermediates made with no prototype, and every property DEFINED
|
|
2552
|
+
// there — nothing a page's script left on a prototype runs or is written to.
|
|
2420
2553
|
fn attach_at_path(
|
|
2421
2554
|
scope: &mut v8::PinScope<'_, '_>,
|
|
2422
|
-
context: v8::Local<v8::Context>,
|
|
2423
2555
|
name: &str,
|
|
2424
2556
|
function: v8::Local<v8::Function>,
|
|
2425
2557
|
) -> Result<JsVal, VmError> {
|
|
2426
|
-
let mut
|
|
2558
|
+
let (mut holder, path, hidden) = path_root(scope, name);
|
|
2559
|
+
let mut parts: Vec<&str> = path.split('.').collect();
|
|
2427
2560
|
let leaf = parts
|
|
2428
2561
|
.pop()
|
|
2429
2562
|
.filter(|s| !s.is_empty())
|
|
2430
2563
|
.ok_or_else(|| VmError::Runtime(format!("invalid attach name `{name}`")))?;
|
|
2431
|
-
|
|
2564
|
+
// Make a property of |holder|: defined on the host object's side, assigned on
|
|
2565
|
+
// the global's (as attach always has, so an accessor there still sees it).
|
|
2566
|
+
let put = |scope: &mut v8::PinScope<'_, '_>,
|
|
2567
|
+
holder: v8::Local<v8::Object>,
|
|
2568
|
+
key: v8::Local<v8::String>,
|
|
2569
|
+
value: v8::Local<v8::Value>| {
|
|
2570
|
+
if hidden {
|
|
2571
|
+
holder.create_data_property(scope, key.into(), value)
|
|
2572
|
+
} else {
|
|
2573
|
+
holder.set(scope, key.into(), value)
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2432
2576
|
for part in parts {
|
|
2433
2577
|
if part.is_empty() {
|
|
2434
2578
|
return Err(VmError::Runtime(format!("invalid attach name `{name}`")));
|
|
2435
2579
|
}
|
|
2436
2580
|
let key = v8::String::new(scope, part)
|
|
2437
2581
|
.ok_or_else(|| VmError::Runtime("name too large".into()))?;
|
|
2438
|
-
holder = match
|
|
2582
|
+
holder = match path_segment(scope, holder, key, hidden) {
|
|
2439
2583
|
Some(v) if v.is_object() => v8::Local::<v8::Object>::try_from(v).unwrap(),
|
|
2440
2584
|
// Don't clobber an existing non-object (e.g. a primitive global that
|
|
2441
2585
|
// collides with the namespace name) — fail loudly instead.
|
|
@@ -2445,8 +2589,12 @@ fn attach_at_path(
|
|
|
2445
2589
|
)));
|
|
2446
2590
|
}
|
|
2447
2591
|
_ => {
|
|
2448
|
-
let obj =
|
|
2449
|
-
|
|
2592
|
+
let obj = if hidden {
|
|
2593
|
+
null_prototype_object(scope)
|
|
2594
|
+
} else {
|
|
2595
|
+
v8::Object::new(scope)
|
|
2596
|
+
};
|
|
2597
|
+
if put(scope, holder, key, obj.into()) != Some(true) {
|
|
2450
2598
|
return Err(VmError::Runtime(format!(
|
|
2451
2599
|
"`{name}`: cannot create `{part}`"
|
|
2452
2600
|
)));
|
|
@@ -2457,7 +2605,7 @@ fn attach_at_path(
|
|
|
2457
2605
|
}
|
|
2458
2606
|
let key =
|
|
2459
2607
|
v8::String::new(scope, leaf).ok_or_else(|| VmError::Runtime("name too large".into()))?;
|
|
2460
|
-
if
|
|
2608
|
+
if put(scope, holder, key, function.into()) != Some(true) {
|
|
2461
2609
|
return Err(VmError::Runtime(format!(
|
|
2462
2610
|
"`{name}`: target is not writable/extensible"
|
|
2463
2611
|
)));
|
|
@@ -4346,6 +4494,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
4346
4494
|
// same surface.
|
|
4347
4495
|
pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
|
|
4348
4496
|
let module = ruby.define_module("RustyRacer")?;
|
|
4497
|
+
// The argument that crosses into JS as the realm's host object (host_object).
|
|
4498
|
+
module.const_set("HOST_OBJECT", ruby.get_inner(&marshal::HOST_OBJECT))?;
|
|
4349
4499
|
|
|
4350
4500
|
// The isolate (VM) + its isolate-level ops; hands out Contexts.
|
|
4351
4501
|
let isolate = module.define_class("Isolate", ruby.class_object())?;
|
|
@@ -9,7 +9,7 @@ use std::collections::HashMap;
|
|
|
9
9
|
use std::ffi::c_void;
|
|
10
10
|
|
|
11
11
|
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
12
|
-
use magnus::value::ReprValue;
|
|
12
|
+
use magnus::value::{Lazy, ReprValue};
|
|
13
13
|
use magnus::{
|
|
14
14
|
Error, ExceptionClass, IntoValue, RArray, RHash, RString, Ruby, TryConvert, Value, prelude::*,
|
|
15
15
|
};
|
|
@@ -78,6 +78,10 @@ pub(crate) enum JsVal {
|
|
|
78
78
|
// Back-reference to an already-emitted container (preserves identity; makes
|
|
79
79
|
// cycles representable instead of truncating at a depth cap).
|
|
80
80
|
Ref(u32),
|
|
81
|
+
// Ruby RustyRacer::HOST_OBJECT <-> a realm's host object (`crate::host_object`):
|
|
82
|
+
// that of the realm an op runs in — a Context's, a Function handle's, a host
|
|
83
|
+
// function's own — both ways. Another realm's host object reads as an object.
|
|
84
|
+
HostObject,
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
// Cycles and sharing are handled by the Ref table (see JsVal::Ref), so this is
|
|
@@ -340,6 +344,14 @@ fn js_to_jsval_d(
|
|
|
340
344
|
// their toString (the spike's primitive escape hatch).
|
|
341
345
|
if value.is_object() && !value.is_function() {
|
|
342
346
|
let obj = v8::Local::<v8::Object>::try_from(value).unwrap();
|
|
347
|
+
// The realm's own host object crosses back as RustyRacer::HOST_OBJECT,
|
|
348
|
+
// not as a Hash of what the bindings keep there. Only an object with no
|
|
349
|
+
// prototype can be it, which keeps the private read off the common path.
|
|
350
|
+
if obj.get_prototype(scope).is_some_and(|p| p.is_null())
|
|
351
|
+
&& crate::existing_host_object(scope).is_some_and(|host| host == obj)
|
|
352
|
+
{
|
|
353
|
+
return JsVal::HostObject;
|
|
354
|
+
}
|
|
343
355
|
let id = match js_container_id(scope, seen, value, obj, depth) {
|
|
344
356
|
Ok(id) => id,
|
|
345
357
|
Err(jsval) => return jsval,
|
|
@@ -539,6 +551,7 @@ fn jsval_to_js_d<'s>(
|
|
|
539
551
|
JsVal::Ref(id) => built
|
|
540
552
|
.get(scope, id)
|
|
541
553
|
.unwrap_or_else(|| v8::undefined(scope).into()),
|
|
554
|
+
JsVal::HostObject => crate::host_object(scope).into(),
|
|
542
555
|
}
|
|
543
556
|
}
|
|
544
557
|
|
|
@@ -669,9 +682,32 @@ fn jsval_to_ruby_d(
|
|
|
669
682
|
.get(id)
|
|
670
683
|
.copied()
|
|
671
684
|
.unwrap_or_else(|| ruby.qnil().as_value()),
|
|
685
|
+
JsVal::HostObject => ruby.get_inner(&HOST_OBJECT),
|
|
672
686
|
})
|
|
673
687
|
}
|
|
674
688
|
|
|
689
|
+
// RustyRacer::HOST_OBJECT: the Ruby value that stands for a realm's host object
|
|
690
|
+
// as it crosses into JS (JsVal::HostObject). The one instance of
|
|
691
|
+
// RustyRacer::HostObject, whose allocator is undefined once it is made, so no
|
|
692
|
+
// other can be; marked for good (Lazy), so it never moves either.
|
|
693
|
+
fn host_object_inspect(_host: Value) -> &'static str {
|
|
694
|
+
"RustyRacer::HOST_OBJECT"
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
pub(crate) static HOST_OBJECT: Lazy<Value> = Lazy::new(|ruby| {
|
|
698
|
+
let class = ruby
|
|
699
|
+
.define_module("RustyRacer")
|
|
700
|
+
.and_then(|module| module.define_class("HostObject", ruby.class_object()))
|
|
701
|
+
.expect("RustyRacer::HostObject is definable");
|
|
702
|
+
class
|
|
703
|
+
.define_method("inspect", magnus::method!(host_object_inspect, 0))
|
|
704
|
+
.expect("HostObject#inspect is definable");
|
|
705
|
+
let host: Value = class.new_instance(()).expect("a HostObject is allocatable");
|
|
706
|
+
host.freeze();
|
|
707
|
+
class.undef_default_alloc_func();
|
|
708
|
+
host
|
|
709
|
+
});
|
|
710
|
+
|
|
675
711
|
// A Ruby String marshalled by its encoding TAG (the tag is the type):
|
|
676
712
|
// - ASCII-8BIT (binary) -> JsVal::Bytes (a JS Uint8Array);
|
|
677
713
|
// - any text encoding -> JsVal::Str (UTF-8). Already-UTF-8 text is taken
|
|
@@ -842,6 +878,11 @@ fn ruby_to_jsval_d(val: Value, seen: &mut RbSeen, depth: u32) -> Result<JsVal, E
|
|
|
842
878
|
}
|
|
843
879
|
// What only a class, or a conversion method, can say.
|
|
844
880
|
//
|
|
881
|
+
// RustyRacer::HOST_OBJECT -> the realm's host object, by identity.
|
|
882
|
+
if val.as_raw() == ruby.get_inner(&HOST_OBJECT).as_raw() {
|
|
883
|
+
return Ok(JsVal::HostObject);
|
|
884
|
+
}
|
|
885
|
+
//
|
|
845
886
|
// Ruby Time -> JS Date. Before the Numeric fallback: magnus's f64 TryConvert
|
|
846
887
|
// coerces a Time via to_f, so it would otherwise marshal as a bare epoch
|
|
847
888
|
// number. Time#to_f is epoch seconds; Date wants ms.
|
|
@@ -1148,4 +1189,38 @@ mod tests {
|
|
|
1148
1189
|
},
|
|
1149
1190
|
);
|
|
1150
1191
|
}
|
|
1192
|
+
|
|
1193
|
+
#[test]
|
|
1194
|
+
fn the_host_object_is_one_per_realm_and_no_property_of_its_global() {
|
|
1195
|
+
in_context(
|
|
1196
|
+
"globalThis.keys = Reflect.ownKeys(globalThis).length",
|
|
1197
|
+
|scope| {
|
|
1198
|
+
let value = jsval_to_js(
|
|
1199
|
+
scope,
|
|
1200
|
+
JsVal::Array {
|
|
1201
|
+
id: 0,
|
|
1202
|
+
items: vec![JsVal::HostObject, JsVal::HostObject],
|
|
1203
|
+
},
|
|
1204
|
+
);
|
|
1205
|
+
let held = crate::host_object(scope);
|
|
1206
|
+
let key = v8::String::new(scope, "held").unwrap();
|
|
1207
|
+
held.create_data_property(scope, key.into(), v8::Integer::new(scope, 7).into());
|
|
1208
|
+
global_set(scope, "v", value);
|
|
1209
|
+
// (…the global's keys grown by `keys` and `v` alone)
|
|
1210
|
+
assert_eq!(
|
|
1211
|
+
as_string(
|
|
1212
|
+
scope,
|
|
1213
|
+
"[v[0] === v[1], v[0].held, Object.getPrototypeOf(v[0]), Reflect.ownKeys(globalThis).length - keys].join()"
|
|
1214
|
+
),
|
|
1215
|
+
"true,7,,2"
|
|
1216
|
+
);
|
|
1217
|
+
// (…another realm's is its own)
|
|
1218
|
+
let other = v8::Context::new(scope, Default::default());
|
|
1219
|
+
let scope = &mut v8::ContextScope::new(scope, other);
|
|
1220
|
+
let fresh = crate::host_object(scope);
|
|
1221
|
+
assert!(fresh != held);
|
|
1222
|
+
assert!(crate::host_object(scope) == fresh);
|
|
1223
|
+
},
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1151
1226
|
}
|
data/ext/rusty_racer/src/ops.rs
CHANGED
|
@@ -157,9 +157,9 @@ pub(crate) enum Request {
|
|
|
157
157
|
EvaluateModule {
|
|
158
158
|
module_id: i32,
|
|
159
159
|
timeout_ms: u64,
|
|
160
|
-
// The name of a function on the module's context's global
|
|
161
|
-
// a module script": upon rejection, report the exception — now or as a
|
|
162
|
-
// the rejection is the op's error, as far as the drain gets.
|
|
160
|
+
// The name of a function on the module's context's global (an `@` path: on its host object) that reports its
|
|
161
|
+
// evaluation's rejection — HTML "run a module script": upon rejection, report the exception — now or as a
|
|
162
|
+
// top-level-await module settles; none: the rejection is the op's error, as far as the drain gets.
|
|
163
163
|
report: Option<String>,
|
|
164
164
|
},
|
|
165
165
|
ModuleNamespace {
|
|
@@ -294,19 +294,20 @@ pub(crate) fn run_source(
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
// Resolve a dotted property path on the current realm's globalThis
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
297
|
+
// Resolve a dotted property path on the current realm's globalThis — or, for an
|
|
298
|
+
// `@` path, on its host object (`host_object`) — to a function and the holder it
|
|
299
|
+
// was read off — its `this`, so `a.b.f` gets the right receiver. Each segment is
|
|
300
|
+
// an ordinary [[Get]] (an `@` path's of an own property alone: path_segment), so
|
|
301
|
+
// a getter or Proxy trap on the path runs; when one throws, the error is
|
|
302
|
+
// Err(None): the caller's TryCatch holds it (`caught!`).
|
|
302
303
|
fn resolve_function<'s>(
|
|
303
304
|
scope: &mut v8::PinScope<'s, '_>,
|
|
304
305
|
path: &str,
|
|
305
306
|
) -> Result<(v8::Local<'s, v8::Function>, v8::Local<'s, v8::Value>), Option<VmError>> {
|
|
306
|
-
let
|
|
307
|
-
let mut holder: v8::Local<v8::Value> =
|
|
308
|
-
let mut target: v8::Local<v8::Value> =
|
|
309
|
-
for part in
|
|
307
|
+
let (root, rest, hidden) = path_root(scope, path);
|
|
308
|
+
let mut holder: v8::Local<v8::Value> = root.into();
|
|
309
|
+
let mut target: v8::Local<v8::Value> = root.into();
|
|
310
|
+
for part in rest.split('.') {
|
|
310
311
|
let Some(obj) = target.to_object(scope) else {
|
|
311
312
|
// The holder of `part` (a preceding segment) was null/undefined, so
|
|
312
313
|
// there's nothing to read `part` from — name the holder, not `part`.
|
|
@@ -317,7 +318,7 @@ fn resolve_function<'s>(
|
|
|
317
318
|
let Some(key) = v8::String::new(scope, part) else {
|
|
318
319
|
return Err(Some(VmError::Runtime("property name too large".into())));
|
|
319
320
|
};
|
|
320
|
-
let Some(next) =
|
|
321
|
+
let Some(next) = path_segment(scope, obj, key, hidden) else {
|
|
321
322
|
return Err(None);
|
|
322
323
|
};
|
|
323
324
|
holder = target;
|
|
@@ -883,8 +884,8 @@ fn op_attach(
|
|
|
883
884
|
timeout_ms: u64,
|
|
884
885
|
outermost: bool,
|
|
885
886
|
) -> VmReply {
|
|
886
|
-
// attach_at_path writes onto globalThis (and walks
|
|
887
|
-
// path), which can fire a user-defined accessor or Proxy trap —
|
|
887
|
+
// attach_at_path writes onto globalThis or the host object (and walks
|
|
888
|
+
// a dotted path), which can fire a user-defined accessor or Proxy trap —
|
|
888
889
|
// arbitrary JS. So it goes through the same bracket as Eval: a
|
|
889
890
|
// host fn the trap calls routes back, and a looping trap is
|
|
890
891
|
// time-capped.
|
|
@@ -908,7 +909,7 @@ fn op_attach(
|
|
|
908
909
|
// under a namespace object, creating missing
|
|
909
910
|
// intermediates, so host fns needn't pollute the
|
|
910
911
|
// bare global.
|
|
911
|
-
Some(function) => attach_at_path(scope,
|
|
912
|
+
Some(function) => attach_at_path(scope, &name, function),
|
|
912
913
|
None => Err(VmError::Runtime("failed to build function".into())),
|
|
913
914
|
};
|
|
914
915
|
auto_drain(scope, outermost);
|
|
@@ -955,7 +956,7 @@ fn op_attach_many(
|
|
|
955
956
|
.data(external.into())
|
|
956
957
|
.build(scope)
|
|
957
958
|
{
|
|
958
|
-
Some(function) => attach_at_path(scope,
|
|
959
|
+
Some(function) => attach_at_path(scope, name, function),
|
|
959
960
|
None => Err(VmError::Runtime(format!(
|
|
960
961
|
"failed to build function for `{name}`"
|
|
961
962
|
))),
|
|
@@ -1272,7 +1273,7 @@ fn op_evaluate_module(
|
|
|
1272
1273
|
did_eval = true;
|
|
1273
1274
|
v8::tc_scope!(let tc, scope);
|
|
1274
1275
|
let reporter =
|
|
1275
|
-
report.as_deref().and_then(|name|
|
|
1276
|
+
report.as_deref().and_then(|name| named_function(tc, name));
|
|
1276
1277
|
match module.evaluate(tc) {
|
|
1277
1278
|
// A synchronous top-level throw yields a
|
|
1278
1279
|
// *rejected* promise (not None); a pending
|
|
@@ -1339,11 +1340,18 @@ fn op_evaluate_module(
|
|
|
1339
1340
|
VmReply::Done(outcome)
|
|
1340
1341
|
}
|
|
1341
1342
|
|
|
1342
|
-
// The function the current context's global has under `name
|
|
1343
|
-
|
|
1343
|
+
// The function the current context's global has under `name` (one property,
|
|
1344
|
+
// dots and all, as it always was) — or, for an `@` path, the one the path names
|
|
1345
|
+
// on its host object, as `Context#function` finds it — or None.
|
|
1346
|
+
fn named_function<'s>(
|
|
1344
1347
|
scope: &mut v8::PinScope<'s, '_>,
|
|
1345
1348
|
name: &str,
|
|
1346
1349
|
) -> Option<v8::Local<'s, v8::Function>> {
|
|
1350
|
+
if name.starts_with('@') {
|
|
1351
|
+
return resolve_function(scope, name)
|
|
1352
|
+
.ok()
|
|
1353
|
+
.map(|(function, _)| function);
|
|
1354
|
+
}
|
|
1347
1355
|
let key = v8::String::new(scope, name)?;
|
|
1348
1356
|
let global = scope.get_current_context().global(scope);
|
|
1349
1357
|
v8::Local::<v8::Function>::try_from(global.get(scope, key.into())?).ok()
|
data/lib/rusty_racer/version.rb
CHANGED
data/lib/rusty_racer.rb
CHANGED
|
@@ -91,10 +91,15 @@ module RustyRacer
|
|
|
91
91
|
# the microtask queue when the outermost eval/call/run/evaluate completes
|
|
92
92
|
# (the standard embedder contract); :explicit drains only on
|
|
93
93
|
# #perform_microtask_checkpoint.
|
|
94
|
+
# host_namespace names the object every realm gets rusty_racer's own
|
|
95
|
+
# functions in (drainMicrotasks, contextGlobal, contextOf, ...): a property
|
|
96
|
+
# of the global (`'MiniRacer'`), or, as `'@MiniRacer'`, of the realm's host
|
|
97
|
+
# object, which no script of a page's can reach (see HOST_OBJECT).
|
|
94
98
|
def self.new(host_namespace: nil, snapshot: nil, timeout_ms: 0, memory_limit: 0, microtasks: :auto)
|
|
95
99
|
unless %i[auto explicit].include?(microtasks)
|
|
96
100
|
raise ArgumentError, "microtasks must be :auto or :explicit, got #{microtasks.inspect}"
|
|
97
101
|
end
|
|
102
|
+
raise ArgumentError, 'host_namespace: "@" names no property of the host object' if host_namespace == '@'
|
|
98
103
|
|
|
99
104
|
_new(host_namespace, snapshot, timeout_ms, memory_limit, microtasks == :explicit)
|
|
100
105
|
end
|
|
@@ -116,6 +121,24 @@ module RustyRacer
|
|
|
116
121
|
end
|
|
117
122
|
end
|
|
118
123
|
|
|
124
|
+
# HOST_OBJECT (defined natively): an argument that crosses into JS as the
|
|
125
|
+
# realm's HOST OBJECT — an object with no prototype that no global holds, which
|
|
126
|
+
# the host and the realm's own bindings share and no other script can reach.
|
|
127
|
+
# Every path the host names (Context#attach / #attach_many / #call /
|
|
128
|
+
# #call_void / #function, Module#evaluate's report:, Isolate.new's
|
|
129
|
+
# host_namespace:) starts from it when it begins with `@`:
|
|
130
|
+
#
|
|
131
|
+
# ctx.attach('@fetch', ->(url) { ... }) # host.fetch, no global
|
|
132
|
+
# ctx.call('installBindings', RustyRacer::HOST_OBJECT)
|
|
133
|
+
# step = ctx.function('@steps.query') # host.steps.query
|
|
134
|
+
#
|
|
135
|
+
# An `@` path walks own properties alone. HOST_OBJECT is the host object of
|
|
136
|
+
# the realm the operation runs in (a Context's, a Function handle's, a host
|
|
137
|
+
# function's own, for its result), and that object crosses back as
|
|
138
|
+
# HOST_OBJECT. Each realm has its own, made as it is first used; a reset makes
|
|
139
|
+
# the realm, and so its host object, anew. Hand it to the bindings before any
|
|
140
|
+
# script of a page's runs, by a name they delete as they take it.
|
|
141
|
+
|
|
119
142
|
# A v8::Context (realm) handed out by an Isolate: where JS actually runs.
|
|
120
143
|
class Context
|
|
121
144
|
# `timeout_ms` (0 = the isolate default) caps this eval; `filename` names the
|
|
@@ -180,9 +203,9 @@ module RustyRacer
|
|
|
180
203
|
|
|
181
204
|
# Evaluate the instantiated module. Its evaluation's rejection — a top-level throw, a top-level await's — raises
|
|
182
205
|
# (RustyRacer::RuntimeError, as far as the drain after it settles), or, `report:` naming a function of its context's
|
|
183
|
-
# global, is that function's to report: called with the rejection's
|
|
184
|
-
# returns, a top-level await's as it settles — and nothing raised (HTML
|
|
185
|
-
# report the exception).
|
|
206
|
+
# global (an `@` path: on its host object, HOST_OBJECT), is that function's to report: called with the rejection's
|
|
207
|
+
# reason — a top-level throw's before this returns, a top-level await's as it settles — and nothing raised (HTML
|
|
208
|
+
# "run a module script": upon rejection, report the exception).
|
|
186
209
|
def evaluate(report: nil)
|
|
187
210
|
_evaluate(report&.to_s)
|
|
188
211
|
end
|