rusty_racer 0.1.0
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 +7 -0
- data/Cargo.toml +5 -0
- data/README.md +188 -0
- data/ext/rusty_racer/Cargo.lock +952 -0
- data/ext/rusty_racer/Cargo.toml +23 -0
- data/ext/rusty_racer/extconf.rb +11 -0
- data/ext/rusty_racer/src/lib.rs +4883 -0
- data/lib/rusty_racer/version.rb +5 -0
- data/lib/rusty_racer.rb +122 -0
- metadata +67 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 724aca7618b9458b51912665ca47d521dfde28b7c1aa96ebb053206919cc4cec
|
|
4
|
+
data.tar.gz: ba40ef90042ceec6fecc771f0c02ec4c3e241c2abf2881a77ab78d9b05bde4f7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 82c58b671e6080fd000bab244a6144a44d9112ab30b2d23ea81e1cf9ea4fa6ee74a18a2e21e636b6d32267aeb6f5502e0c7cf6002f503d27aa7d46be201d0b78
|
|
7
|
+
data.tar.gz: 89773bf8f89d17192c17114518a7eb8e9a6dd041b1b0e3e3ac52d8c2cc274f2b9a9e5aa1d59a4a39c1f15cdeb304ab851055efd65bab21c9ce859acdf12c9549
|
data/Cargo.toml
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# rusty_racer
|
|
2
|
+
|
|
3
|
+
Embed [V8](https://v8.dev/) in Ruby, built on [rusty_v8](https://crates.io/crates/v8)
|
|
4
|
+
(the `v8` crate) and [Magnus](https://github.com/matsadler/magnus) via
|
|
5
|
+
[rb-sys](https://github.com/oxidize-rb/rb-sys).
|
|
6
|
+
|
|
7
|
+
> Early and experimental — the API still moves. Each `Isolate` runs V8 in-thread
|
|
8
|
+
> on the Ruby thread that created it (the GVL is released around the JS run), and
|
|
9
|
+
> is **thread-confined**: every operation must happen on that owner thread, or it
|
|
10
|
+
> raises. `Isolate#terminate` is the exception — it is safe from any thread.
|
|
11
|
+
|
|
12
|
+
## What it can do
|
|
13
|
+
|
|
14
|
+
Names follow V8's: an `Isolate` is the VM; it hands out `Context`s (v8::Context,
|
|
15
|
+
a realm) that you run JS in.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "rusty_racer"
|
|
19
|
+
|
|
20
|
+
iso = RustyRacer::Isolate.new(timeout_ms: 1000)
|
|
21
|
+
ctx = iso.context # the default context
|
|
22
|
+
|
|
23
|
+
ctx.eval("1 + 1") # => 2
|
|
24
|
+
ctx.eval("({a: 1, b: [true, 'x']})") # => {"a"=>1, "b"=>[true, "x"]}
|
|
25
|
+
|
|
26
|
+
# Call a JS function with marshalled args (BigInt/Date/Map/Set/shared refs
|
|
27
|
+
# all round-trip faithfully).
|
|
28
|
+
ctx.eval("function add(a, b) { return a + b }")
|
|
29
|
+
ctx.call("add", 20, 22) # => 42
|
|
30
|
+
ctx.call_void("doSideEffect") # runs it; never marshals the return
|
|
31
|
+
|
|
32
|
+
# Ruby callbacks into JS; a raised Ruby exception becomes a JS exception.
|
|
33
|
+
ctx.attach("rubyUpcase", ->(s) { s.upcase })
|
|
34
|
+
ctx.eval("rubyUpcase('hi')") # => "HI"
|
|
35
|
+
|
|
36
|
+
# Stack traces: JS errors carry the JS stack as the Ruby backtrace.
|
|
37
|
+
begin
|
|
38
|
+
ctx.eval("throw new Error('boom')", filename: "app.js")
|
|
39
|
+
rescue RustyRacer::RuntimeError => e
|
|
40
|
+
e.message # => "Error: boom"
|
|
41
|
+
e.backtrace # => ["app.js:1:7"] (named frames read "app.js:1:25:in 'fn'")
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
ES modules (the embedder owns the URL→module registry):
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
dep = ctx.compile_module("export const x = 21;", filename: "/dep.js")
|
|
49
|
+
app = ctx.compile_module('import {x} from "./dep.js"; export const r = x * 2;',
|
|
50
|
+
filename: "/app.js")
|
|
51
|
+
app.instantiate { |specifier, referrer| dep if specifier == "./dep.js" }
|
|
52
|
+
app.evaluate
|
|
53
|
+
app.namespace["r"] # => 42
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Classic `<script>`s work the same way: `ctx.compile("1 + 1").run` # => 2.
|
|
57
|
+
|
|
58
|
+
### Bytecode caching
|
|
59
|
+
|
|
60
|
+
V8 compiles lazily: the top level up front, each function body on first call.
|
|
61
|
+
Caches can be produced two ways, matching that.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
src = "function double(x) { return x * 2 }; double(21)"
|
|
65
|
+
|
|
66
|
+
# produce_cache: — a cold cache taken at compile time (top level only). Persist
|
|
67
|
+
# it, then pass it back via cached_data: to skip the reparse on the next boot,
|
|
68
|
+
# even in another process or isolate.
|
|
69
|
+
blob = ctx.compile(src, produce_cache: true).cached_data
|
|
70
|
+
other = RustyRacer::Isolate.new.context.compile(src, cached_data: blob)
|
|
71
|
+
other.cache_rejected? # => false (true if the blob was stale)
|
|
72
|
+
|
|
73
|
+
# create_code_cache — a warm cache from the current compile state. Run a script
|
|
74
|
+
# (or evaluate a module) first, and it also captures the inner functions that
|
|
75
|
+
# actually ran — the warm cache a browser keeps; produce_cache can't see them.
|
|
76
|
+
s = ctx.compile(src)
|
|
77
|
+
s.run
|
|
78
|
+
warm = s.create_code_cache # binary String, or nil if V8 can't serialize
|
|
79
|
+
|
|
80
|
+
# eager: compiles every function up front instead of lazily (~2× compile time,
|
|
81
|
+
# more memory) — worth it only when producing a cache. Ignored with cached_data:.
|
|
82
|
+
ctx.compile(src, produce_cache: true, eager: true)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both `compile` (classic scripts → `Script#run`) and `compile_module` (ES modules
|
|
86
|
+
→ `#instantiate`/`#evaluate`) take `cached_data:`, `produce_cache:`, and `eager:`,
|
|
87
|
+
and expose `#cached_data` / `#cache_rejected?` / `#create_code_cache`.
|
|
88
|
+
|
|
89
|
+
Also available:
|
|
90
|
+
|
|
91
|
+
- **`Snapshot`** — startup blobs: boot an isolate from a baked-in heap and code
|
|
92
|
+
cache.
|
|
93
|
+
- **`Isolate#create_context`** — an extra realm with its own globals, sharing the
|
|
94
|
+
isolate's heap. All realms are mutually same-origin (with a host namespace,
|
|
95
|
+
`NS.contextGlobal(id)` reaches another realm's `globalThis`, like a same-origin
|
|
96
|
+
`iframe.contentWindow`), so this is **not** an isolation boundary.
|
|
97
|
+
- **`Isolate#perform_microtask_checkpoint`** — manual microtask drain. The default
|
|
98
|
+
`microtasks: :auto` also drains at the end of each outermost eval/call/evaluate;
|
|
99
|
+
`microtasks: :explicit` leaves it fully manual. There is no event loop or timers
|
|
100
|
+
either way.
|
|
101
|
+
- **`Isolate#terminate`**, **`Isolate#dynamic_import_resolver=`**,
|
|
102
|
+
**`Context#reset`** (below), and **`Platform.set_flags!`**.
|
|
103
|
+
|
|
104
|
+
### `Context#reset`
|
|
105
|
+
|
|
106
|
+
`reset` swaps the realm's `globalThis` for a fresh `v8::Context`, reusing the
|
|
107
|
+
warm isolate — a per-visit reset that avoids rebuilding the VM. Its contract:
|
|
108
|
+
|
|
109
|
+
- **The snapshot is replayed.** On a snapshotted isolate the fresh context is
|
|
110
|
+
re-deserialized from the snapshot, so the snapshot's baked-in globals — and
|
|
111
|
+
its precompiled code cache — come back. `reset` means "back to the snapshot"
|
|
112
|
+
(or to an empty realm, with no snapshot).
|
|
113
|
+
- **Runtime mutations are dropped.** Anything set on the realm at runtime is gone.
|
|
114
|
+
- **Host fns are dropped.** Functions `attach`/`attach_many`'d into the realm are
|
|
115
|
+
released (their GC roots freed); re-attach them after a reset.
|
|
116
|
+
- **Modules and classic scripts are dropped.** Handles compiled in the realm die
|
|
117
|
+
with the old context.
|
|
118
|
+
- **The realm id and the shared same-origin token are preserved** — the id keeps
|
|
119
|
+
addressing the realm, now backed by the fresh context.
|
|
120
|
+
- **`reset` is refused (raises), leaving the realm untouched, when** a microtask
|
|
121
|
+
checkpoint is draining, the realm is unknown/disposed, or a request for it is
|
|
122
|
+
suspended on the V8 stack (e.g. resetting a realm from inside one of its own
|
|
123
|
+
host fns).
|
|
124
|
+
|
|
125
|
+
## Threading
|
|
126
|
+
|
|
127
|
+
An `Isolate` runs V8 **in-thread** on the Ruby thread that created it, and is
|
|
128
|
+
**thread-confined**: every operation on it — and on the `Context`s, `Module`s,
|
|
129
|
+
and `Script`s it hands out — must run on that owner thread. A V8 isolate is bound
|
|
130
|
+
to one native thread (rusty_v8 exposes no `v8::Locker`), so using it from another
|
|
131
|
+
thread raises `RustyRacer::WrongThreadError` rather than corrupting the VM.
|
|
132
|
+
|
|
133
|
+
- **`Isolate#terminate` is the one exception** — it is safe to call from any
|
|
134
|
+
thread (it stops a runaway script on the owner thread).
|
|
135
|
+
- **Dispose on the owner thread.** `Isolate#dispose` must run on the owner
|
|
136
|
+
thread. If the last reference to an isolate is instead garbage-collected on a
|
|
137
|
+
*different* thread (e.g. its owner thread already exited), it cannot be
|
|
138
|
+
disposed and the V8 isolate **leaks** until the process exits. To avoid this,
|
|
139
|
+
call `iso.dispose` on the owner thread before that thread ends — and watch
|
|
140
|
+
`RustyRacer.leaked_isolate_count` (and `RustyRacer.live_isolate_count`) to
|
|
141
|
+
confirm a long-running, thread-churning workload isn't leaking.
|
|
142
|
+
|
|
143
|
+
One isolate per thread is the supported model; share work between threads by
|
|
144
|
+
giving each thread its own isolate.
|
|
145
|
+
|
|
146
|
+
### Fibers
|
|
147
|
+
|
|
148
|
+
In-thread V8 runs on whatever stack the calling Ruby code is on — including a
|
|
149
|
+
**Fiber**'s separate stack (a plain `Enumerator` is a Fiber, so this is common:
|
|
150
|
+
`Capybara::Result#find`, lazy enumerators, …). This works on the **main thread**,
|
|
151
|
+
where the process stack is the highest address and every Fiber sits below it.
|
|
152
|
+
|
|
153
|
+
On a **non-main thread** it does not: V8 anchors its "is this the central stack?"
|
|
154
|
+
check to that thread's native stack top (a pthread value it caches, with no API
|
|
155
|
+
to retarget), and a Fiber allocated *above* that top — the usual case off the
|
|
156
|
+
main thread — falls outside the check, so V8 aborts the process on the next GC or
|
|
157
|
+
thrown exception. So **don't call into an isolate from inside a Fiber on a
|
|
158
|
+
worker thread**; drive isolate ops directly on the thread, or keep
|
|
159
|
+
Fiber/Enumerator-mediated JS calls on the main thread.
|
|
160
|
+
|
|
161
|
+
## Installation
|
|
162
|
+
|
|
163
|
+
Precompiled gems bundle V8 — no V8 build, no Rust toolchain — for Ruby 3.3, 3.4,
|
|
164
|
+
and 4.0 on:
|
|
165
|
+
|
|
166
|
+
- **Linux:** x86_64 and arm64 (aarch64)
|
|
167
|
+
- **macOS:** arm64 (Apple silicon) and x86_64 (Intel)
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
gem "rusty_racer"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
or `gem install rusty_racer`. On any other platform or Ruby, the source gem
|
|
174
|
+
builds the extension at install time — see below.
|
|
175
|
+
|
|
176
|
+
## Building from source
|
|
177
|
+
|
|
178
|
+
The stock `v8` crate prebuilt links as a binary (initial-exec TLS), which a Ruby
|
|
179
|
+
extension's shared object can't use. A source build therefore needs a
|
|
180
|
+
**library-TLS** `librusty_v8.a`. Either:
|
|
181
|
+
|
|
182
|
+
- point `RUSTY_V8_ARCHIVE` at a prebuilt library-TLS archive, or
|
|
183
|
+
- set `V8_FROM_SOURCE=1` to build V8 from the `denoland/rusty_v8` git tree
|
|
184
|
+
(large: lots of disk, RAM, and time).
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT.
|