rusty_racer 0.2.4 → 0.2.6
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 +60 -0
- data/README.md +79 -1
- data/ext/rusty_racer/Cargo.toml +6 -1
- data/ext/rusty_racer/src/lib.rs +596 -174
- data/ext/rusty_racer/src/marshal.rs +356 -117
- data/ext/rusty_racer/src/ops.rs +353 -184
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +18 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebbfddd8204b62eed228d53d476247e3b76bf08a894b02fb3e8ffe78836b6b02
|
|
4
|
+
data.tar.gz: 77d0190f0387cbdf240d49ef4ac3b75c1550a18a74de8ceed78a8c97c1956d96
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0a3df4af6404d9357a008e2adcb4d8303a39db35ad9d3d4ef0240217222bd0fd0be40c9b18b36b9778e30e9eac8a5ce9b20514ecc962975cc25b1466d504aac
|
|
7
|
+
data.tar.gz: 497c28687de4bc14c7b666e99a3205406e84384459b923dca24b8d2080db552d3266bbffbfbecfb0a4e61bde9c17aa4413f39e321d1c866339df6944782c3930
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.6
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `Context#function(path)` returns a `RustyRacer::Function`: the function a
|
|
8
|
+
dotted path names, resolved once, with the object it was read off kept as its
|
|
9
|
+
`this`. `#call` / `#call_void` invoke it as `Context#call` / `#call_void`
|
|
10
|
+
would — same marshalling, timeout, errors and re-entrancy — without walking
|
|
11
|
+
the path or making a property name per call. It raises `DisposedError` once
|
|
12
|
+
its realm is reset or disposed and never keeps that realm alive; `#dispose`
|
|
13
|
+
releases it early, and garbage collection releases it too.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Ruby → JS marshalling no longer runs the page's JavaScript. Arrays and
|
|
18
|
+
objects built for JS have their elements and entries defined
|
|
19
|
+
(`CreateDataProperty`) rather than assigned, so an accessor on
|
|
20
|
+
`Array.prototype` or `Object.prototype` can no longer intercept or swallow
|
|
21
|
+
call arguments, host function results or the host namespace's members. A
|
|
22
|
+
`"__proto__"` key in a Hash now becomes an own property of the object, as
|
|
23
|
+
`JSON.parse` makes it, instead of setting its prototype.
|
|
24
|
+
- Ruby → JS marshalling dispatches on the value's built-in type first: a String,
|
|
25
|
+
Symbol, Array or Hash argument no longer pays for a raised-and-rescued
|
|
26
|
+
conversion probe or a constant lookup (a String argument cost ~1.2 µs more
|
|
27
|
+
than none; now ~0.07 µs). Subclasses, `to_f` (an `ActiveSupport::Duration`),
|
|
28
|
+
`to_str`, `to_ary` and `to_hash` conversions marshal as before — except that
|
|
29
|
+
an Array or Hash subclass is now always an array or an object, where one that
|
|
30
|
+
also defined `to_str` (or a Hash subclass `to_ary`) used to cross as that.
|
|
31
|
+
- A getter or `Proxy` trap that throws while `Context#call` resolves its path
|
|
32
|
+
now raises a `RuntimeError` carrying the JS stack, and the thrown value
|
|
33
|
+
propagates through a host function like any other JS throw.
|
|
34
|
+
- A `Module` or `Script` whose wrapper is garbage-collected without `#dispose`
|
|
35
|
+
is released too (at the end of the isolate's next operation), where 0.2.5 kept
|
|
36
|
+
it until its realm was reset or disposed. `#dispose` itself now only queues
|
|
37
|
+
the release, as `Function#dispose` does, so it works from any thread — in
|
|
38
|
+
0.2.5 a dispose from another thread marked the wrapper disposed but silently
|
|
39
|
+
kept the module or script for good.
|
|
40
|
+
- A module's url (its `import.meta.url`, its url as a referrer) is kept for as
|
|
41
|
+
long as V8 keeps the module, not as long as its wrapper or its realm: a
|
|
42
|
+
dependency the resolver compiled and Ruby dropped or disposed, or a function
|
|
43
|
+
of a disposed realm's module that another realm still holds, still reads it.
|
|
44
|
+
In 0.2.5 either read `undefined`.
|
|
45
|
+
|
|
46
|
+
### Documented
|
|
47
|
+
|
|
48
|
+
- What reading a JS value into Ruby runs (getters, `Proxy` traps, holes read
|
|
49
|
+
through the prototype chain, `toString`) and the binary-String ↔ `Uint8Array`
|
|
50
|
+
rule — README, "Marshalling and the page's JavaScript".
|
|
51
|
+
|
|
52
|
+
### Deliberately unchanged
|
|
53
|
+
|
|
54
|
+
- JS → Ruby still renders a value with no structural form — a function, a
|
|
55
|
+
RegExp, a depth-truncated object — through its ToString, which a page can
|
|
56
|
+
override (`Function.prototype.toString`) to run code or throw during the read.
|
|
57
|
+
V8's side-effect-free renderer (`Value::ToDetailString`, already used for
|
|
58
|
+
Symbols) would run nothing, but it truncates a function's source past 128
|
|
59
|
+
characters and renders objects as `#<Object>`, so switching is a behaviour
|
|
60
|
+
change left for a release that decides it on purpose.
|
data/README.md
CHANGED
|
@@ -67,6 +67,11 @@ ctx.call("add", 20, 22) # => 42
|
|
|
67
67
|
ctx.call_void("doSideEffect") # runs it; never marshals the return
|
|
68
68
|
ctx.eval_void("globalThis.app = boot()") # ditto for the completion value
|
|
69
69
|
|
|
70
|
+
# Resolve a function once and keep it: no path walk or name per call, and its
|
|
71
|
+
# holder stays its `this` (see "Function handles" below).
|
|
72
|
+
add = ctx.function("add")
|
|
73
|
+
add.call(20, 22) # => 42
|
|
74
|
+
|
|
70
75
|
# Ruby callbacks into JS; a raised Ruby exception becomes a JS exception.
|
|
71
76
|
ctx.attach("rubyUpcase", ->(s) { s.upcase })
|
|
72
77
|
ctx.eval("rubyUpcase('hi')") # => "HI"
|
|
@@ -93,6 +98,65 @@ ran to completion. When the value is incidental, say so: `#eval_void`,
|
|
|
93
98
|
result. A statement list has a completion value whether you want one or not, so
|
|
94
99
|
plumbing an object into a global (`globalThis.win = someProxy`) is enough to
|
|
95
100
|
make a plain `#eval` fail after every one of its writes has landed.
|
|
101
|
+
`#eval_primitive` sits between the two: it returns the completion value only
|
|
102
|
+
where it is a primitive, and nil for an object or a symbol, which it never reads.
|
|
103
|
+
|
|
104
|
+
### Function handles
|
|
105
|
+
|
|
106
|
+
`Context#function(path)` resolves a dotted path (`"app.router.go"`) the way
|
|
107
|
+
`#call` does — each segment an ordinary property read off the context's
|
|
108
|
+
`globalThis` — once, and returns a `RustyRacer::Function` bound to what it found:
|
|
109
|
+
the function, and the object it was read off as its `this`. `#call(*args)` and
|
|
110
|
+
`#call_void(*args)` then invoke it exactly as `Context#call` / `#call_void` would
|
|
111
|
+
— same argument and result marshalling, same timeout, same errors, and they work
|
|
112
|
+
from inside a host function like any other op — without walking the path or
|
|
113
|
+
making a property name per call. Reassigning or deleting the path afterwards
|
|
114
|
+
does not change what the handle calls.
|
|
115
|
+
|
|
116
|
+
A handle lives as long as its realm does. Once its context is `reset` or
|
|
117
|
+
`dispose`d (or the isolate is), `#call` raises `RustyRacer::DisposedError`, and
|
|
118
|
+
the handle no longer keeps that realm alive. (`#disposed?` answers for the handle
|
|
119
|
+
and its isolate only: a realm's reset is found by the next call.) `#dispose` lets
|
|
120
|
+
go of one early, and a handle that is simply garbage-collected is released too,
|
|
121
|
+
at the end of the isolate's next operation. A path that leads into *another*
|
|
122
|
+
realm (`frames[0].fn` in an embedder whose realms see each other) holds that
|
|
123
|
+
realm's function for as long as the handle's own realm lives, as any reference
|
|
124
|
+
kept there would.
|
|
125
|
+
|
|
126
|
+
### Releasing handles
|
|
127
|
+
|
|
128
|
+
A `Function`, `Script` or `Module` handle is released when it is `#dispose`d —
|
|
129
|
+
from any thread — or garbage-collected, at the end of the isolate's next
|
|
130
|
+
operation, and with its realm's `reset` or `dispose` at the latest. Releasing a
|
|
131
|
+
handle only drops the isolate's hold on what it names: functions a script
|
|
132
|
+
defined keep working, and a module V8 still keeps — linked into a live graph, or
|
|
133
|
+
reached from another realm — keeps its `import.meta.url` and its url as a
|
|
134
|
+
referrer, wrapper or realm or not.
|
|
135
|
+
|
|
136
|
+
### Marshalling and the page's JavaScript
|
|
137
|
+
|
|
138
|
+
Values cross by copy, not by reference.
|
|
139
|
+
|
|
140
|
+
- **Ruby → JS builds new values without running any JavaScript.** An array's
|
|
141
|
+
elements and an object's entries are *defined* on the new container
|
|
142
|
+
(`CreateDataProperty`), never assigned, so an accessor a script has put on
|
|
143
|
+
`Array.prototype[0]` or `Object.prototype.x` neither sees nor swallows them,
|
|
144
|
+
and a `"__proto__"` key is an own property, as `JSON.parse` makes it. Maps and
|
|
145
|
+
Sets are filled by V8's original builtins, whatever `Map.prototype.set` is now.
|
|
146
|
+
- **JS → Ruby reads values the way a script would.** An object's own enumerable
|
|
147
|
+
string keys are listed and each is read with an ordinary `[[Get]]`, so a getter
|
|
148
|
+
runs; a `Proxy` is read through its `ownKeys`, `getOwnPropertyDescriptor` and
|
|
149
|
+
`get` traps; an array's holes read through its prototype chain, as `arr[i]`
|
|
150
|
+
does; a function, a RegExp or any other value with no structural form becomes
|
|
151
|
+
its `toString()`, which may be the page's. A throw anywhere in that fails the op
|
|
152
|
+
(see above), and `#call_void` / `#eval_void` skip the read entirely. Map and Set
|
|
153
|
+
contents, typed arrays and buffers are read without running JavaScript.
|
|
154
|
+
- **A String's encoding tag is its type.** A binary (`ASCII-8BIT`) String crosses
|
|
155
|
+
as a `Uint8Array`, and a `Uint8Array`, `ArrayBuffer` or `SharedArrayBuffer`
|
|
156
|
+
comes back as a binary String. Any text encoding crosses as a JS string, and
|
|
157
|
+
must hold valid characters — invalid UTF-8 raises `EncodingError` rather than
|
|
158
|
+
turn into U+FFFD — so text read with `File.binread` or off a socket needs its
|
|
159
|
+
real encoding (`force_encoding`) before it crosses as text.
|
|
96
160
|
|
|
97
161
|
ES modules (the embedder owns the URL→module registry):
|
|
98
162
|
|
|
@@ -114,6 +178,12 @@ loop here (see below), a `true` graph whose `await` never settles returns from
|
|
|
114
178
|
`evaluate` with the module body still suspended, and `status` reports
|
|
115
179
|
`:evaluated` either way — so `graph_async?` is what distinguishes the two.
|
|
116
180
|
|
|
181
|
+
A rejected evaluation — a top-level throw, or a top-level `await` that rejects —
|
|
182
|
+
raises from `evaluate`. Pass `report:` the name of a function on the context's
|
|
183
|
+
global to have it called with the reason instead (HTML's "report the
|
|
184
|
+
exception"), at once for a throw and as it settles for an `await`:
|
|
185
|
+
`app.evaluate(report: "reportError")`.
|
|
186
|
+
|
|
117
187
|
Classic `<script>`s work the same way: `ctx.compile("1 + 1").run` # => 2.
|
|
118
188
|
|
|
119
189
|
### Resource limits
|
|
@@ -203,6 +273,12 @@ Also available:
|
|
|
203
273
|
`microtasks: :auto` also drains at the end of each outermost eval/call/evaluate;
|
|
204
274
|
`microtasks: :explicit` leaves it fully manual. There is no event loop or timers
|
|
205
275
|
either way.
|
|
276
|
+
- **`Isolate#pump_message_loop`** — runs the tasks V8 posts instead of running
|
|
277
|
+
inline, most visibly `FinalizationRegistry` cleanup callbacks: a microtask
|
|
278
|
+
checkpoint never runs them, so without a pump a collected target's callback
|
|
279
|
+
never fires. It drains every pending task without blocking, then a microtask
|
|
280
|
+
checkpoint; with nothing pending it is a no-op. The callbacks are ordinary JS,
|
|
281
|
+
so the pump is capped by `timeout_ms` like an `eval`.
|
|
206
282
|
- **`Isolate#terminate`**, **`Isolate#dynamic_import_resolver=`**,
|
|
207
283
|
**`Context#reset`** (below), and **`Platform.set_flags!`**.
|
|
208
284
|
|
|
@@ -218,7 +294,9 @@ warm isolate — a per-visit reset that avoids rebuilding the VM. Its contract:
|
|
|
218
294
|
- **Runtime mutations are dropped.** Anything set on the realm at runtime is gone.
|
|
219
295
|
- **Host fns are dropped.** Functions `attach`/`attach_many`'d into the realm are
|
|
220
296
|
released — the isolate stops holding them, so whatever they captured becomes
|
|
221
|
-
collectable; re-attach them after a reset.
|
|
297
|
+
collectable; re-attach them after a reset. One the embedder kept hold of (through
|
|
298
|
+
another realm, say) throws `unknown host function` when called; it never runs
|
|
299
|
+
what was attached after it.
|
|
222
300
|
- **Modules and classic scripts are dropped.** Handles compiled in the realm die
|
|
223
301
|
with the old context.
|
|
224
302
|
- **The realm id and the shared same-origin token are preserved** — the id keeps
|
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.6"
|
|
9
9
|
edition = "2024"
|
|
10
10
|
publish = false
|
|
11
11
|
|
|
@@ -31,3 +31,8 @@ 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
32
|
# current native thread (in-thread V8 runs on the Ruby thread's stack).
|
|
33
33
|
libc = "0.2"
|
|
34
|
+
|
|
35
|
+
# `cargo test` links libruby: the crate calls into it, though the marshalling tests
|
|
36
|
+
# that run without a VM never do.
|
|
37
|
+
[dev-dependencies]
|
|
38
|
+
rb-sys = { version = "0.9", features = ["link-ruby"] }
|