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 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
@@ -0,0 +1,5 @@
1
+ # Workspace root so rb-sys's `cargo metadata` (run from the gem root by
2
+ # RbSys::ExtensionTask) finds the extension crate.
3
+ [workspace]
4
+ members = ["ext/rusty_racer"]
5
+ resolver = "2"
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.