rusty_racer 0.2.6 → 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 +48 -0
- data/README.md +56 -0
- data/ext/rusty_racer/Cargo.toml +4 -3
- 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/ext/rusty_racer/src/stack.rs +383 -107
- 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,53 @@
|
|
|
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
|
+
|
|
36
|
+
## 0.2.7
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- An operation on a Ruby Fiber looks its stack's bounds up once per stack
|
|
41
|
+
rather than once per operation that a 16-entry cache had dropped. Capybara
|
|
42
|
+
filters each result set in an Enumerator left suspended until GC frees it, so
|
|
43
|
+
a page of finds runs on hundreds of distinct fiber stacks, and each lookup read
|
|
44
|
+
all of `/proc/self/maps` — a fifth of a find loop, and slower the larger
|
|
45
|
+
Ruby's fiber pool grew. Every stack an operation has run on is now kept (Ruby
|
|
46
|
+
hands its pooled stacks out again and never unmaps them), and on Linux 6.11+ a
|
|
47
|
+
lookup asks the kernel for the one mapping (`PROCMAP_QUERY`) instead of
|
|
48
|
+
reading the whole map. A call into V8 from a fresh fiber, steady state: 160 µs
|
|
49
|
+
→ 1.0 µs beside a 2,200-line map, 521 µs → 1.0 µs beside 8,300 lines.
|
|
50
|
+
|
|
3
51
|
## 0.2.6
|
|
4
52
|
|
|
5
53
|
### Added
|
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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# links and loads with no custom archive.
|
|
6
6
|
[package]
|
|
7
7
|
name = "rusty_racer"
|
|
8
|
-
version = "0.2.
|
|
8
|
+
version = "0.2.8"
|
|
9
9
|
edition = "2024"
|
|
10
10
|
publish = false
|
|
11
11
|
|
|
@@ -29,8 +29,9 @@ rb-sys = "0.9"
|
|
|
29
29
|
# (which is built with rusty_v8_enable_simdutf=true) on every platform.
|
|
30
30
|
v8 = { version = "150.4.0", features = ["simdutf"] }
|
|
31
31
|
# pthread stack-bounds query, to set V8's stack limit per op relative to the
|
|
32
|
-
# current native thread (in-thread V8 runs on the Ruby thread's stack).
|
|
33
|
-
|
|
32
|
+
# current native thread (in-thread V8 runs on the Ruby thread's stack). 0.2.172
|
|
33
|
+
# for `_IOWR`, which builds the PROCMAP_QUERY ioctl number per architecture.
|
|
34
|
+
libc = "0.2.172"
|
|
34
35
|
|
|
35
36
|
# `cargo test` links libruby: the crate calls into it, though the marshalling tests
|
|
36
37
|
# that run without a VM never do.
|
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()
|
|
@@ -151,33 +151,24 @@ fn native_stack_bounds() -> (usize, usize) {
|
|
|
151
151
|
(0, 0)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// (0, 0) if unknown.
|
|
154
|
+
// The BOTTOM of the stack `addr` is on, and the highest address an op has been
|
|
155
|
+
// seen running at on it. Used for a Ruby Fiber, whose mmap'd stack pthread can't
|
|
156
|
+
// see: V8's limit must sit ABOVE this bottom or a deep fiber recursion overflows
|
|
157
|
+
// the real stack and SEGVs the unmapped guard. (0, 0) if unknown.
|
|
159
158
|
//
|
|
160
|
-
// Cached per native thread,
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
159
|
+
// Cached per native thread, for EVERY stack an op has run on, because a miss
|
|
160
|
+
// asks the OS and a workload can run each op on a stack it has never used before:
|
|
161
|
+
// Capybara filters each result set in an Enumerator, left suspended until GC
|
|
162
|
+
// frees it, so a thousand finds run on a thousand distinct fiber stacks. A cache
|
|
163
|
+
// of sixteen slots missed on nearly every one of them, and where a miss reads all
|
|
164
|
+
// of /proc/self/maps — whose length grows with Ruby's fiber pool — those reads
|
|
165
|
+
// were a fifth of the find loop (1003 per 1000 finds). Keeping every stack makes
|
|
166
|
+
// the steady state free: the pool hands the same stacks out again (its free list
|
|
167
|
+
// is LIFO, and it never unmaps them — below), so once each has had an op on it
|
|
168
|
+
// nothing misses. The table is as large as the set of stacks ops have run on,
|
|
169
|
+
// which that pool bounds.
|
|
168
170
|
//
|
|
169
|
-
//
|
|
170
|
-
// ops cycling through more fibers than there are slots are exactly the pattern
|
|
171
|
-
// those policies handle worst, since each eviction takes the entry needed next
|
|
172
|
-
// and the hit rate collapses to zero — one fiber over the size measured as slow
|
|
173
|
-
// as having no cache at all. Evicting at random keeps roughly
|
|
174
|
-
// slots/live-fibers of the accesses, so going over the size degrades gradually
|
|
175
|
-
// instead of falling off a cliff. Empty slots are filled before anything is
|
|
176
|
-
// evicted. Entries are never invalidated: a dead fiber's keeps its slot until
|
|
177
|
-
// some miss draws it, and with a live set that fits there are no misses at all
|
|
178
|
-
// — which is why a hit has to be self-justifying rather than merely plausible.
|
|
179
|
-
//
|
|
180
|
-
// So the mapping's top is never used, or even kept, as an upper bound. A fiber
|
|
171
|
+
// The mapping's top is never used, or even kept, as an upper bound. A fiber
|
|
181
172
|
// stack's mapping can extend far above the stack itself, where the kernel merged
|
|
182
173
|
// it with an adjacent anonymous mapping of the same protection that has nothing
|
|
183
174
|
// to do with the fiber pool (measured: a 644KB stack inside an 8840KB mapping,
|
|
@@ -207,94 +198,160 @@ fn native_stack_bounds() -> (usize, usize) {
|
|
|
207
198
|
// Within that window a cached bottom is the fiber's real bottom: Ruby brackets
|
|
208
199
|
// every fiber stack with PROT_NONE guard pages, so the mapping cannot start
|
|
209
200
|
// below the stack, and a pooled slot is handed to the next fiber with identical
|
|
210
|
-
// bounds.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
201
|
+
// bounds. Entries are never invalidated, and that rests on the pool too: Ruby
|
|
202
|
+
// frees a pool allocation only when built with FIBER_POOL_ALLOCATION_FREE (cont.c
|
|
203
|
+
// defines it for a limited address space, i.e. 32-bit, which V8 doesn't ship for,
|
|
204
|
+
// and for RB_EXPERIMENTAL_FIBER_POOL), so the bounds an op saw on a stack stay
|
|
205
|
+
// the bounds of every later fiber there. Under a build that does unmap, an entry
|
|
206
|
+
// can outlive its memory; a hit is then still right when the memory is mapped
|
|
207
|
+
// again as the same pool's stacks at the same address — the same stride, so the
|
|
208
|
+
// same guards — and wrong, with a bottom below the new one, when a later mapping
|
|
209
|
+
// puts a guard page inside the old window. The same goes for a stack that is not
|
|
210
|
+
// Ruby's: a C extension's own coroutine stack, unmapped once it is done with and
|
|
211
|
+
// its addresses reused, can leave an entry that answers for memory it no longer
|
|
212
|
+
// describes. Ruby's M:N thread stacks are safe the way its fibers are: a freed
|
|
213
|
+
// one is only madvised, never unmapped.
|
|
214
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
215
|
+
struct FiberStack {
|
|
216
|
+
// The bottom of the stack's mapping.
|
|
216
217
|
lo: usize,
|
|
217
218
|
// Highest address a V8 op has run at on this stack.
|
|
218
219
|
seen: usize,
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
|
|
222
|
+
// Sorted by `lo`, with disjoint [lo, seen] windows — so `seen` ascends too, and
|
|
223
|
+
// both lookups below are binary searches.
|
|
224
|
+
struct FiberStacks(Vec<FiberStack>);
|
|
225
|
+
|
|
226
|
+
impl FiberStacks {
|
|
227
|
+
// The entry whose window holds `addr`.
|
|
228
|
+
fn find(&self, addr: usize) -> Option<FiberStack> {
|
|
229
|
+
let below = self.0.partition_point(|s| s.lo <= addr);
|
|
230
|
+
let stack = *self.0.get(below.checked_sub(1)?)?;
|
|
231
|
+
(addr <= stack.seen).then_some(stack)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// (bottom, highest-address-seen) of the stack `addr` is on: the entry's when
|
|
235
|
+
// one holds it, else `query`'s answer for `addr`, recorded — 0 there being
|
|
236
|
+
// "unknown", which is recorded nowhere.
|
|
237
|
+
fn bounds(&mut self, addr: usize, query: impl FnOnce(usize) -> usize) -> (usize, usize) {
|
|
238
|
+
if let Some(stack) = self.find(addr) {
|
|
239
|
+
return (stack.lo, stack.seen);
|
|
240
|
+
}
|
|
241
|
+
let lo = query(addr);
|
|
242
|
+
if lo == 0 {
|
|
243
|
+
return (0, 0);
|
|
244
|
+
}
|
|
245
|
+
self.record(lo, addr);
|
|
246
|
+
(lo, addr)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Record an op at `addr` on the stack whose bottom was just looked up as
|
|
250
|
+
// `lo`. Being fresh, that replaces every entry overlapping [lo, addr]: the
|
|
251
|
+
// same stack's narrower window, or one describing memory since mapped afresh.
|
|
252
|
+
// Every query checks that its mapping holds `addr`, so `lo <= addr`.
|
|
253
|
+
fn record(&mut self, lo: usize, addr: usize) {
|
|
254
|
+
debug_assert!(lo <= addr);
|
|
255
|
+
let from = self.0.partition_point(|s| s.seen < lo);
|
|
256
|
+
let to = self.0.partition_point(|s| s.lo <= addr);
|
|
257
|
+
self.0.splice(from..to, [FiberStack { lo, seen: addr }]);
|
|
258
|
+
}
|
|
228
259
|
}
|
|
229
260
|
|
|
230
261
|
thread_local! {
|
|
231
|
-
static
|
|
232
|
-
std::cell::RefCell::new(
|
|
233
|
-
slots: [FiberRegion { lo: 0, seen: 0 }; FIBER_REGION_SLOTS],
|
|
234
|
-
rng: 0x9E37_79B9_7F4A_7C15,
|
|
235
|
-
})
|
|
236
|
-
};
|
|
262
|
+
static FIBER_STACKS: std::cell::RefCell<FiberStacks> =
|
|
263
|
+
const { std::cell::RefCell::new(FiberStacks(Vec::new())) };
|
|
237
264
|
}
|
|
238
265
|
|
|
239
266
|
// (bottom, highest-address-seen) of the stack `addr` is on. `addr` must be an
|
|
240
267
|
// address the CALLER knows is on that stack and as high up it as it can offer —
|
|
241
268
|
// it both answers the lookup and becomes the entry's `seen` on a miss, so a
|
|
242
|
-
// caller that passes its topmost frame gets the widest sound window.
|
|
243
|
-
//
|
|
244
|
-
// whole table out on every fiber op.
|
|
269
|
+
// caller that passes its topmost frame gets the widest sound window. An op run
|
|
270
|
+
// while the thread's locals are being torn down gets the fail-safe (0, 0).
|
|
245
271
|
fn current_region_bounds_cached(addr: usize) -> (usize, usize) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
.slots
|
|
250
|
-
.iter()
|
|
251
|
-
.find(|r| r.lo != 0 && addr >= r.lo && addr <= r.seen)
|
|
252
|
-
{
|
|
253
|
-
return (r.lo, r.seen);
|
|
254
|
-
}
|
|
255
|
-
let (lo, _) = query_region_bounds(addr);
|
|
256
|
-
if lo == 0 {
|
|
257
|
-
return (0, 0);
|
|
258
|
-
}
|
|
259
|
-
let mut cache = c.borrow_mut();
|
|
260
|
-
// Prefer the entry for a stack we already know (a frame above anything
|
|
261
|
-
// seen on it so far, which just widens the window), then an empty slot,
|
|
262
|
-
// and only then evict.
|
|
263
|
-
let slot = cache
|
|
264
|
-
.slots
|
|
265
|
-
.iter()
|
|
266
|
-
.position(|r| r.lo == lo)
|
|
267
|
-
.or_else(|| cache.slots.iter().position(|r| r.lo == 0))
|
|
268
|
-
.unwrap_or_else(|| {
|
|
269
|
-
cache.rng ^= cache.rng << 13;
|
|
270
|
-
cache.rng ^= cache.rng >> 7;
|
|
271
|
-
cache.rng ^= cache.rng << 17;
|
|
272
|
-
(cache.rng % FIBER_REGION_SLOTS as u64) as usize
|
|
273
|
-
});
|
|
274
|
-
cache.slots[slot] = FiberRegion { lo, seen: addr };
|
|
275
|
-
(lo, addr)
|
|
276
|
-
})
|
|
272
|
+
FIBER_STACKS
|
|
273
|
+
.try_with(|stacks| stacks.borrow_mut().bounds(addr, query_region_bottom))
|
|
274
|
+
.unwrap_or((0, 0))
|
|
277
275
|
}
|
|
278
276
|
|
|
279
|
-
// The
|
|
280
|
-
// established. Only called on a cache miss
|
|
277
|
+
// The bottom of the mapping containing `addr`, or 0 when it can't be
|
|
278
|
+
// established. Only called on a cache miss: a stack no op has run on yet, or a
|
|
279
|
+
// frame above any seen on it.
|
|
281
280
|
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
281
|
+
// 0 is the fail-safe answer, not an error: every caller falls back to a fixed
|
|
282
|
+
// budget below the SP, which is what this gem did on every platform before any
|
|
283
|
+
// of these lookups existed. So a platform without an implementation, or a lookup
|
|
284
|
+
// that returns something we can't vouch for, degrades to a smaller JS stack
|
|
285
|
+
// rather than to a wrong one.
|
|
287
286
|
//
|
|
288
|
-
// Linux
|
|
287
|
+
// Linux asks the kernel for the one mapping (PROCMAP_QUERY, 6.11+: ~0.65us for
|
|
288
|
+
// the open, the ioctl and the close, measured beside 3000 mappings) and reads
|
|
289
|
+
// all of /proc/self/maps only where the ioctl isn't there — ~25-200us, growing
|
|
290
|
+
// with the map. The file is opened per call rather than kept: an open file
|
|
291
|
+
// would go on describing the PARENT's address space in a forked child.
|
|
292
|
+
#[cfg(target_os = "linux")]
|
|
293
|
+
fn query_region_bottom(addr: usize) -> usize {
|
|
294
|
+
let Ok(mut maps) = std::fs::File::open("/proc/self/maps") else {
|
|
295
|
+
return 0;
|
|
296
|
+
};
|
|
297
|
+
procmap_query_bottom(&maps, addr).unwrap_or_else(|| read_maps_bottom(&mut maps, addr))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The bottom of the mapping containing `addr`, through the PROCMAP_QUERY ioctl
|
|
301
|
+
// on an open /proc/self/maps; None when the ioctl fails, which on a live stack
|
|
302
|
+
// address means a kernel without it (ENOTTY), or answers with a mapping that
|
|
303
|
+
// does not hold `addr`, which the kernel's never does but an emulated /proc
|
|
304
|
+
// might.
|
|
289
305
|
#[cfg(target_os = "linux")]
|
|
290
|
-
fn
|
|
306
|
+
fn procmap_query_bottom(maps: &std::fs::File, addr: usize) -> Option<usize> {
|
|
307
|
+
use std::os::fd::AsRawFd;
|
|
308
|
+
// struct procmap_query, from linux/fs.h — not in libc. Only `vma_start` is
|
|
309
|
+
// read back, but the ioctl number encodes the whole struct's size.
|
|
310
|
+
#[repr(C)]
|
|
311
|
+
#[derive(Default)]
|
|
312
|
+
struct ProcmapQuery {
|
|
313
|
+
size: u64,
|
|
314
|
+
query_flags: u64,
|
|
315
|
+
query_addr: u64,
|
|
316
|
+
vma_start: u64,
|
|
317
|
+
vma_end: u64,
|
|
318
|
+
vma_flags: u64,
|
|
319
|
+
vma_page_size: u64,
|
|
320
|
+
vma_offset: u64,
|
|
321
|
+
inode: u64,
|
|
322
|
+
dev_major: u32,
|
|
323
|
+
dev_minor: u32,
|
|
324
|
+
vma_name_size: u32,
|
|
325
|
+
build_id_size: u32,
|
|
326
|
+
vma_name_addr: u64,
|
|
327
|
+
build_id_addr: u64,
|
|
328
|
+
}
|
|
329
|
+
// Flags 0 ask for the mapping COVERING query_addr, not the next one above it.
|
|
330
|
+
const PROCMAP_QUERY: libc::Ioctl = libc::_IOWR::<ProcmapQuery>(b'f' as u32, 17);
|
|
331
|
+
let mut query = ProcmapQuery {
|
|
332
|
+
size: std::mem::size_of::<ProcmapQuery>() as u64,
|
|
333
|
+
query_addr: addr as u64,
|
|
334
|
+
..Default::default()
|
|
335
|
+
};
|
|
336
|
+
let rc = unsafe {
|
|
337
|
+
libc::ioctl(
|
|
338
|
+
maps.as_raw_fd(),
|
|
339
|
+
PROCMAP_QUERY,
|
|
340
|
+
&mut query as *mut ProcmapQuery,
|
|
341
|
+
)
|
|
342
|
+
};
|
|
343
|
+
let (lo, hi) = (query.vma_start as usize, query.vma_end as usize);
|
|
344
|
+
(rc == 0 && lo <= addr && addr < hi).then_some(lo)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// The bottom of the mapping containing `addr`, from the text of
|
|
348
|
+
// /proc/self/maps, or 0 if no line covers it.
|
|
349
|
+
#[cfg(target_os = "linux")]
|
|
350
|
+
fn read_maps_bottom(maps: &mut std::fs::File, addr: usize) -> usize {
|
|
291
351
|
use std::io::Read;
|
|
292
352
|
let mut buf = String::new();
|
|
293
|
-
if
|
|
294
|
-
|
|
295
|
-
.is_err()
|
|
296
|
-
{
|
|
297
|
-
return (0, 0);
|
|
353
|
+
if maps.read_to_string(&mut buf).is_err() {
|
|
354
|
+
return 0;
|
|
298
355
|
}
|
|
299
356
|
for line in buf.lines() {
|
|
300
357
|
// e.g. "7f6a...000-7f6a...000 rw-p 00000000 00:00 0 ..."
|
|
@@ -309,10 +366,10 @@ fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
|
309
366
|
continue;
|
|
310
367
|
};
|
|
311
368
|
if addr >= lo && addr < hi {
|
|
312
|
-
return
|
|
369
|
+
return lo;
|
|
313
370
|
}
|
|
314
371
|
}
|
|
315
|
-
|
|
372
|
+
0
|
|
316
373
|
}
|
|
317
374
|
|
|
318
375
|
// macOS has no /proc, so ask the Mach VM directly. mach_vm_region reports the
|
|
@@ -322,7 +379,7 @@ fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
|
322
379
|
// that isn't tells us we either walked into something that is not a stack or
|
|
323
380
|
// misread the reply, in which case the fixed-budget fallback is the safer answer.
|
|
324
381
|
#[cfg(target_os = "macos")]
|
|
325
|
-
fn
|
|
382
|
+
fn query_region_bottom(addr: usize) -> usize {
|
|
326
383
|
// Not in libc, which points at the `mach2` crate instead; these are the only
|
|
327
384
|
// entry points we need, so declare them here rather than take the dependency.
|
|
328
385
|
// (libc does carry mach_task_self_, but deprecated for the same reason.)
|
|
@@ -370,20 +427,20 @@ fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
|
370
427
|
mach_port_deallocate(mach_task_self_, object_name);
|
|
371
428
|
}
|
|
372
429
|
if kr != KERN_SUCCESS || size == 0 || info[0] & VM_PROT_READ_WRITE != VM_PROT_READ_WRITE {
|
|
373
|
-
return
|
|
430
|
+
return 0;
|
|
374
431
|
}
|
|
375
432
|
let (lo, hi) = (start as usize, start.saturating_add(size) as usize);
|
|
376
433
|
if addr >= lo && addr < hi {
|
|
377
|
-
|
|
434
|
+
lo
|
|
378
435
|
} else {
|
|
379
|
-
|
|
436
|
+
0 // `addr` was in a hole; this is the region after it
|
|
380
437
|
}
|
|
381
438
|
}
|
|
382
439
|
}
|
|
383
440
|
|
|
384
441
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
385
|
-
fn
|
|
386
|
-
|
|
442
|
+
fn query_region_bottom(_addr: usize) -> usize {
|
|
443
|
+
0
|
|
387
444
|
}
|
|
388
445
|
|
|
389
446
|
// Set from RUSTY_RACER_STACK_DEBUG at init; gates the per-op stack diagnostics.
|
|
@@ -517,7 +574,7 @@ impl<'a> StackScope<'a> {
|
|
|
517
574
|
} else {
|
|
518
575
|
// Region unknown. Both shipped platforms implement the lookup,
|
|
519
576
|
// so reaching this on one of them means the lookup FAILED (see
|
|
520
|
-
//
|
|
577
|
+
// query_region_bottom for what it refuses to vouch for) — worth
|
|
521
578
|
// chasing rather than assuming, since the budget here is only a
|
|
522
579
|
// guess: a fixed reserve below the SP, hoping the fiber is that
|
|
523
580
|
// deep. It usually is for an OUTERMOST fiber op, whose SP sits
|
|
@@ -596,12 +653,15 @@ impl<'a> StackScope<'a> {
|
|
|
596
653
|
// stack (the cache's `seen`, NOT the mapping top, which can reach far
|
|
597
654
|
// into an unrelated neighbour — see current_region_bounds_cached);
|
|
598
655
|
// otherwise we cannot tell it is on our stack at all, and adopting it
|
|
599
|
-
// would send the scan up through our guard page.
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
656
|
+
// would send the scan up through our guard page. The enclosing op
|
|
657
|
+
// ran on this stack at or below `seen` — it widened the entry to its
|
|
658
|
+
// own `stack_top` if nothing had — and an entry is only ever replaced
|
|
659
|
+
// by a fresh lookup overlapping it, which for a stack still mapped is
|
|
660
|
+
// the same stack with a higher `seen`. So the test holds for every
|
|
661
|
+
// enclosing op on our stack whose lookup went through this thread's
|
|
662
|
+
// table; one that ran on another native thread (an M:N Ruby thread
|
|
663
|
+
// that resumed elsewhere inside a host callback) is unknown here, and
|
|
664
|
+
// the scan narrows to this op's own frame — the residual below.
|
|
605
665
|
let new_scan_start = if on_native {
|
|
606
666
|
unsafe { v8__base__Stack__GetStackStart() }
|
|
607
667
|
} else if nested && region.0 != 0 && prev_scan_start > sp && prev_scan_start <= region.1 {
|
|
@@ -655,3 +715,219 @@ impl Drop for StackScope<'_> {
|
|
|
655
715
|
}
|
|
656
716
|
}
|
|
657
717
|
}
|
|
718
|
+
|
|
719
|
+
#[cfg(test)]
|
|
720
|
+
mod tests {
|
|
721
|
+
use super::*;
|
|
722
|
+
|
|
723
|
+
fn stacks(entries: &[(usize, usize)]) -> FiberStacks {
|
|
724
|
+
FiberStacks(
|
|
725
|
+
entries
|
|
726
|
+
.iter()
|
|
727
|
+
.map(|&(lo, seen)| FiberStack { lo, seen })
|
|
728
|
+
.collect(),
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#[test]
|
|
733
|
+
fn a_lookup_hits_only_inside_a_window() {
|
|
734
|
+
let table = stacks(&[(0x1000, 0x1800), (0x3000, 0x3400)]);
|
|
735
|
+
assert_eq!(table.find(0x0fff), None);
|
|
736
|
+
assert_eq!(
|
|
737
|
+
table.find(0x1000),
|
|
738
|
+
Some(FiberStack {
|
|
739
|
+
lo: 0x1000,
|
|
740
|
+
seen: 0x1800
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
assert_eq!(table.find(0x1800).map(|s| s.lo), Some(0x1000));
|
|
744
|
+
// Above `seen` is not known to be on that stack, though no other entry
|
|
745
|
+
// claims it.
|
|
746
|
+
assert_eq!(table.find(0x1801), None);
|
|
747
|
+
assert_eq!(table.find(0x3200).map(|s| s.lo), Some(0x3000));
|
|
748
|
+
assert_eq!(table.find(0x3401), None);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#[test]
|
|
752
|
+
fn a_stack_is_looked_up_once_and_again_only_above_its_window() {
|
|
753
|
+
let mut table = stacks(&[]);
|
|
754
|
+
let queries = std::cell::Cell::new(0);
|
|
755
|
+
let mut bounds = |addr| {
|
|
756
|
+
table.bounds(addr, |_| {
|
|
757
|
+
queries.set(queries.get() + 1);
|
|
758
|
+
0x1000
|
|
759
|
+
})
|
|
760
|
+
};
|
|
761
|
+
assert_eq!(bounds(0x1800), (0x1000, 0x1800));
|
|
762
|
+
assert_eq!(bounds(0x1400), (0x1000, 0x1800));
|
|
763
|
+
assert_eq!(bounds(0x1800), (0x1000, 0x1800));
|
|
764
|
+
assert_eq!(queries.get(), 1);
|
|
765
|
+
assert_eq!(bounds(0x1c00), (0x1000, 0x1c00));
|
|
766
|
+
assert_eq!(bounds(0x1900), (0x1000, 0x1c00));
|
|
767
|
+
assert_eq!(queries.get(), 2);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
#[test]
|
|
771
|
+
fn an_unknown_bottom_is_not_recorded() {
|
|
772
|
+
let mut table = stacks(&[]);
|
|
773
|
+
assert_eq!(table.bounds(0x1800, |_| 0), (0, 0));
|
|
774
|
+
assert_eq!(table.0, Vec::new());
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
#[test]
|
|
778
|
+
fn a_frame_above_the_window_widens_the_same_entry() {
|
|
779
|
+
let mut table = stacks(&[(0x1000, 0x1800), (0x3000, 0x3400)]);
|
|
780
|
+
table.record(0x1000, 0x1c00);
|
|
781
|
+
assert_eq!(table.0, stacks(&[(0x1000, 0x1c00), (0x3000, 0x3400)]).0);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
#[test]
|
|
785
|
+
fn a_new_stack_is_inserted_in_order() {
|
|
786
|
+
let mut table = stacks(&[(0x1000, 0x1800), (0x5000, 0x5400)]);
|
|
787
|
+
table.record(0x3000, 0x3100);
|
|
788
|
+
table.record(0x0100, 0x0200);
|
|
789
|
+
table.record(0x9000, 0x9100);
|
|
790
|
+
assert_eq!(
|
|
791
|
+
table.0,
|
|
792
|
+
stacks(&[
|
|
793
|
+
(0x0100, 0x0200),
|
|
794
|
+
(0x1000, 0x1800),
|
|
795
|
+
(0x3000, 0x3100),
|
|
796
|
+
(0x5000, 0x5400),
|
|
797
|
+
(0x9000, 0x9100)
|
|
798
|
+
])
|
|
799
|
+
.0
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
#[test]
|
|
804
|
+
fn a_fresh_bottom_replaces_every_entry_it_overlaps() {
|
|
805
|
+
// Memory once holding two stacks, now one mapping from 0x1400: both old
|
|
806
|
+
// windows reach into [0x1400, 0x3200], so neither may answer again.
|
|
807
|
+
let mut table = stacks(&[
|
|
808
|
+
(0x0100, 0x0200),
|
|
809
|
+
(0x1000, 0x1800),
|
|
810
|
+
(0x3000, 0x3400),
|
|
811
|
+
(0x5000, 0x5400),
|
|
812
|
+
]);
|
|
813
|
+
table.record(0x1400, 0x3200);
|
|
814
|
+
assert_eq!(
|
|
815
|
+
table.0,
|
|
816
|
+
stacks(&[(0x0100, 0x0200), (0x1400, 0x3200), (0x5000, 0x5400)]).0
|
|
817
|
+
);
|
|
818
|
+
assert_eq!(table.find(0x1200), None);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// A mapping laid out as Ruby's fiber pool lays a stack out: read-write
|
|
822
|
+
// pages with PROT_NONE guard pages among them. Unmapped on drop.
|
|
823
|
+
#[cfg(target_os = "linux")]
|
|
824
|
+
struct GuardedStack {
|
|
825
|
+
base: usize,
|
|
826
|
+
len: usize,
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#[cfg(target_os = "linux")]
|
|
830
|
+
impl GuardedStack {
|
|
831
|
+
fn page() -> usize {
|
|
832
|
+
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// `pages` pages, of which those at the `guards` indexes are PROT_NONE.
|
|
836
|
+
fn map(pages: usize, guards: &[usize]) -> Self {
|
|
837
|
+
let len = pages * Self::page();
|
|
838
|
+
let stack = Self {
|
|
839
|
+
base: Self::mmap(0, len, 0),
|
|
840
|
+
len,
|
|
841
|
+
};
|
|
842
|
+
stack.protect(guards);
|
|
843
|
+
stack
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Map the same addresses afresh (MAP_FIXED replaces the old mapping in
|
|
847
|
+
// one step, so no other thread's mmap can land in between), with other
|
|
848
|
+
// guards.
|
|
849
|
+
fn remap(&self, guards: &[usize]) {
|
|
850
|
+
assert_eq!(Self::mmap(self.base, self.len, libc::MAP_FIXED), self.base);
|
|
851
|
+
self.protect(guards);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
fn mmap(at: usize, len: usize, flags: libc::c_int) -> usize {
|
|
855
|
+
let base = unsafe {
|
|
856
|
+
libc::mmap(
|
|
857
|
+
at as *mut c_void,
|
|
858
|
+
len,
|
|
859
|
+
libc::PROT_READ | libc::PROT_WRITE,
|
|
860
|
+
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | flags,
|
|
861
|
+
-1,
|
|
862
|
+
0,
|
|
863
|
+
)
|
|
864
|
+
};
|
|
865
|
+
assert_ne!(base, libc::MAP_FAILED);
|
|
866
|
+
base as usize
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
fn protect(&self, guards: &[usize]) {
|
|
870
|
+
for &guard in guards {
|
|
871
|
+
let page = self.page_at(guard) as *mut c_void;
|
|
872
|
+
assert_eq!(
|
|
873
|
+
unsafe { libc::mprotect(page, Self::page(), libc::PROT_NONE) },
|
|
874
|
+
0
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
fn page_at(&self, index: usize) -> usize {
|
|
880
|
+
self.base + index * Self::page()
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
#[cfg(target_os = "linux")]
|
|
885
|
+
impl Drop for GuardedStack {
|
|
886
|
+
fn drop(&mut self) {
|
|
887
|
+
unsafe { libc::munmap(self.base as *mut c_void, self.len) };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Both ways of asking agree with each other and with the layout.
|
|
892
|
+
#[cfg(target_os = "linux")]
|
|
893
|
+
fn assert_bottom(addr: usize, expected: usize) {
|
|
894
|
+
assert_eq!(query_region_bottom(addr), expected);
|
|
895
|
+
let mut maps = std::fs::File::open("/proc/self/maps").unwrap();
|
|
896
|
+
assert_eq!(read_maps_bottom(&mut maps, addr), expected);
|
|
897
|
+
// Older kernels have no PROCMAP_QUERY; where there is one it must agree.
|
|
898
|
+
if let Some(bottom) = procmap_query_bottom(&maps, addr) {
|
|
899
|
+
assert_eq!(bottom, expected);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
#[cfg(target_os = "linux")]
|
|
904
|
+
#[test]
|
|
905
|
+
fn the_bottom_is_the_page_above_the_guard() {
|
|
906
|
+
let stack = GuardedStack::map(8, &[0]);
|
|
907
|
+
assert_bottom(stack.page_at(5) + 17, stack.page_at(1));
|
|
908
|
+
assert_bottom(stack.page_at(1), stack.page_at(1));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
#[cfg(target_os = "linux")]
|
|
912
|
+
#[test]
|
|
913
|
+
fn a_stack_mapped_again_with_other_guards_has_its_new_bottom() {
|
|
914
|
+
// Unmap a fiber's stack and map the same addresses again with a guard
|
|
915
|
+
// inside the old one: a lookup is a fresh question, so it answers with
|
|
916
|
+
// the new bottom, never the one the old mapping had.
|
|
917
|
+
let stack = GuardedStack::map(8, &[0]);
|
|
918
|
+
assert_bottom(stack.page_at(6), stack.page_at(1));
|
|
919
|
+
stack.remap(&[0, 4]);
|
|
920
|
+
assert_bottom(stack.page_at(6), stack.page_at(5));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
#[cfg(target_os = "linux")]
|
|
924
|
+
#[test]
|
|
925
|
+
fn a_live_stack_is_found_where_it_runs() {
|
|
926
|
+
// This thread's own stack: the frame is somewhere above its bottom.
|
|
927
|
+
let marker = 0u8;
|
|
928
|
+
let addr = &marker as *const u8 as usize;
|
|
929
|
+
let bottom = query_region_bottom(addr);
|
|
930
|
+
assert_ne!(bottom, 0);
|
|
931
|
+
assert!(bottom <= addr);
|
|
932
|
+
}
|
|
933
|
+
}
|
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
|