mini_racer_rs 1.0.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/AI-DISCLOSURE.md +13 -0
- data/CHANGELOG.md +45 -0
- data/COMPATIBILITY.md +235 -0
- data/Cargo.lock +1263 -0
- data/Cargo.toml +62 -0
- data/LICENSE +21 -0
- data/README.md +254 -0
- data/THIRD_PARTY_NOTICES.md +4501 -0
- data/crates/mini_racer_engine/Cargo.toml +25 -0
- data/crates/mini_racer_engine/build.rs +13 -0
- data/crates/mini_racer_engine/src/context.rs +802 -0
- data/crates/mini_racer_engine/src/control.rs +239 -0
- data/crates/mini_racer_engine/src/error.rs +88 -0
- data/crates/mini_racer_engine/src/fast_calls.rs +681 -0
- data/crates/mini_racer_engine/src/lib.rs +20 -0
- data/crates/mini_racer_engine/src/notify.rs +107 -0
- data/crates/mini_racer_engine/src/process/code_cache.rs +412 -0
- data/crates/mini_racer_engine/src/process/fork.rs +261 -0
- data/crates/mini_racer_engine/src/process/platform.rs +194 -0
- data/crates/mini_racer_engine/src/process/tasks.rs +96 -0
- data/crates/mini_racer_engine/src/process/watchdog.rs +185 -0
- data/crates/mini_racer_engine/src/process/workers.rs +165 -0
- data/crates/mini_racer_engine/src/process.rs +69 -0
- data/crates/mini_racer_engine/src/runner.rs +1560 -0
- data/crates/mini_racer_engine/src/serialize.rs +253 -0
- data/crates/mini_racer_engine/src/snapshot.rs +221 -0
- data/crates/mini_racer_engine/src/spin.rs +23 -0
- data/crates/mini_racer_engine/src/sys.rs +566 -0
- data/crates/mini_racer_sync/Cargo.toml +18 -0
- data/crates/mini_racer_sync/src/channel.rs +621 -0
- data/crates/mini_racer_sync/src/fork.rs +116 -0
- data/crates/mini_racer_sync/src/lib.rs +20 -0
- data/crates/mini_racer_sync/src/lock.rs +49 -0
- data/crates/mini_racer_sync/src/parker.rs +44 -0
- data/crates/mini_racer_sync/src/primitives.rs +10 -0
- data/crates/mini_racer_sync/src/tasks.rs +345 -0
- data/crates/mini_racer_sync/src/termination.rs +1234 -0
- data/crates/mini_racer_sync/src/workers.rs +283 -0
- data/crates/mini_racer_wire/Cargo.toml +18 -0
- data/crates/mini_racer_wire/src/decode.rs +926 -0
- data/crates/mini_racer_wire/src/lib.rs +67 -0
- data/crates/mini_racer_wire/src/marker.rs +43 -0
- data/crates/mini_racer_wire/src/text.rs +106 -0
- data/crates/mini_racer_wire/src/write.rs +187 -0
- data/ext/mini_racer_rs/Cargo.toml +26 -0
- data/ext/mini_racer_rs/build.rs +16 -0
- data/ext/mini_racer_rs/extconf.rb +78 -0
- data/ext/mini_racer_rs/src/context.rs +485 -0
- data/ext/mini_racer_rs/src/decode.rs +450 -0
- data/ext/mini_racer_rs/src/encode.rs +333 -0
- data/ext/mini_racer_rs/src/errors.rs +156 -0
- data/ext/mini_racer_rs/src/fork.rs +31 -0
- data/ext/mini_racer_rs/src/gc_root.rs +117 -0
- data/ext/mini_racer_rs/src/guard.rs +7 -0
- data/ext/mini_racer_rs/src/host.rs +339 -0
- data/ext/mini_racer_rs/src/lib.rs +44 -0
- data/ext/mini_racer_rs/src/options.rs +269 -0
- data/ext/mini_racer_rs/src/platform.rs +42 -0
- data/ext/mini_racer_rs/src/rb_raw.rs +368 -0
- data/ext/mini_racer_rs/src/snapshot.rs +149 -0
- data/ext/mini_racer_rs/v8-archives.sha256 +8 -0
- data/lib/mini_racer/shared.rb +420 -0
- data/lib/mini_racer/truffleruby.rb +439 -0
- data/lib/mini_racer/version.rb +6 -0
- data/lib/mini_racer.rb +3 -0
- data/lib/mini_racer_rs/internal_error.rb +7 -0
- data/lib/mini_racer_rs/version.rb +5 -0
- data/lib/mini_racer_rs/vocabulary.rb +105 -0
- data/lib/mini_racer_rs.rb +33 -0
- data/licenses/mini_racer.txt +21 -0
- data/licenses/rust/SOURCES.tsv +6 -0
- data/licenses/rust/std/COPYRIGHT +34 -0
- data/licenses/rust/std/COPYRIGHT-library.html +29357 -0
- data/licenses/rust/std/LICENSE-APACHE +176 -0
- data/licenses/rust/std/LICENSE-MIT +25 -0
- data/licenses/v8/SOURCES.tsv +20 -0
- data/licenses/v8/abseil-cpp/LICENSE +203 -0
- data/licenses/v8/chromium-build/LICENSE +27 -0
- data/licenses/v8/components.tsv +29 -0
- data/licenses/v8/dragonbox/LICENSE-Boost +23 -0
- data/licenses/v8/fast_float/LICENSE-MIT +27 -0
- data/licenses/v8/highway/LICENSE +371 -0
- data/licenses/v8/icu/LICENSE +568 -0
- data/licenses/v8/inspector_protocol/LICENSE +27 -0
- data/licenses/v8/libc++/LICENSE.TXT +311 -0
- data/licenses/v8/libc++abi/LICENSE.TXT +311 -0
- data/licenses/v8/llvm-libc/LICENSE.TXT +278 -0
- data/licenses/v8/rapidhash/LICENSE +34 -0
- data/licenses/v8/simdutf/LICENSE +18 -0
- data/licenses/v8/siphash/LICENSE +116 -0
- data/licenses/v8/utf8-decoder/LICENSE +19 -0
- data/licenses/v8/v8/LICENSE +61 -0
- data/licenses/v8/v8/LICENSE.fdlibm +6 -0
- data/licenses/v8/v8/LICENSE.strongtalk +29 -0
- data/licenses/v8/v8/LICENSE.v8 +26 -0
- metadata +156 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
use std::ffi::c_void;
|
|
2
|
+
use std::ptr::NonNull;
|
|
3
|
+
use std::time::Duration;
|
|
4
|
+
|
|
5
|
+
// The prebuilt V8 archive marks every heap mapping MADV_DONTFORK, so a forked
|
|
6
|
+
// child would lose the isolate's memory. `-Wl,--wrap=madvise` (build.rs) routes
|
|
7
|
+
// V8's calls here; this drops that one advice and forwards the rest unchanged.
|
|
8
|
+
#[cfg(all(target_os = "linux", not(feature = "no-madvise-wrap")))]
|
|
9
|
+
#[unsafe(no_mangle)]
|
|
10
|
+
pub extern "C" fn __wrap_madvise(
|
|
11
|
+
addr: *mut libc::c_void,
|
|
12
|
+
len: libc::size_t,
|
|
13
|
+
advice: libc::c_int,
|
|
14
|
+
) -> libc::c_int {
|
|
15
|
+
let forwarded = std::panic::catch_unwind(|| {
|
|
16
|
+
if advice == libc::MADV_DONTFORK {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
// SAFETY: the arguments are the caller's own madvise arguments, passed
|
|
20
|
+
// through unchanged to the kernel, which validates them.
|
|
21
|
+
let result = unsafe { libc::syscall(libc::SYS_madvise, addr, len, advice) };
|
|
22
|
+
libc::c_int::try_from(result).unwrap_or(-1)
|
|
23
|
+
});
|
|
24
|
+
forwarded.unwrap_or_else(|_| std::process::abort())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Turns a runner's isolate into one any thread can lock, so it can be served
|
|
28
|
+
/// without being held between requests. A refused isolate comes back for its
|
|
29
|
+
/// runner to drop.
|
|
30
|
+
pub fn share(isolate: v8::OwnedIsolate) -> Result<v8::SharedIsolate, v8::OwnedIsolate> {
|
|
31
|
+
// SAFETY: the runner calls this on the thread that created `isolate`, with
|
|
32
|
+
// no reference from `Global::open` live, and the only slot it sets, an
|
|
33
|
+
// `Arc<Runtime>`, is Send.
|
|
34
|
+
let shared = unsafe { isolate.try_into_shared() };
|
|
35
|
+
shared.map_err(v8::IntoSharedError::into_isolate)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// V8's view of `blob`, its own bytes rather than a copy. Whoever starts an
|
|
39
|
+
/// isolate from it keeps `blob` until that isolate is gone: a runner's control
|
|
40
|
+
/// block holds its context's blob, and a snapshot build holds its base.
|
|
41
|
+
pub fn startup_data(blob: &crate::SnapshotBlob) -> v8::StartupData {
|
|
42
|
+
let bytes = blob.as_bytes();
|
|
43
|
+
// SAFETY: a blob's bytes never move or change while it lives, and V8 reads
|
|
44
|
+
// them only while an isolate started from them lives, which the blob
|
|
45
|
+
// outlives as the doc comment requires of every caller.
|
|
46
|
+
let bytes: &'static [u8] = unsafe { std::slice::from_raw_parts(bytes.as_ptr(), bytes.len()) };
|
|
47
|
+
v8::StartupData::from(bytes)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// The address V8 names `isolate` by when it posts the isolate a task.
|
|
51
|
+
#[must_use]
|
|
52
|
+
pub fn isolate_key(isolate: &v8::Isolate) -> usize {
|
|
53
|
+
// SAFETY: the pointer is only compared as an address, never dereferenced.
|
|
54
|
+
let raw = unsafe { isolate.as_raw_isolate_ptr() };
|
|
55
|
+
// SAFETY: `UnsafeRawIsolatePtr` is a transparent wrapper around the
|
|
56
|
+
// pointer V8 passes to its task runners.
|
|
57
|
+
let pointer = unsafe { std::mem::transmute::<v8::UnsafeRawIsolatePtr, *mut c_void>(raw) };
|
|
58
|
+
pointer.addr()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Has V8 report each garbage collection on `isolate`, so its runner can
|
|
62
|
+
/// check the heap against `max_memory`.
|
|
63
|
+
pub fn watch_heap(isolate: &mut v8::Isolate) {
|
|
64
|
+
isolate.add_gc_epilogue_callback(after_gc, std::ptr::null_mut(), v8::GCType::kGCTypeAll);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
unsafe extern "C" fn after_gc(
|
|
68
|
+
mut isolate: v8::UnsafeRawIsolatePtr,
|
|
69
|
+
_type: v8::GCType,
|
|
70
|
+
_flags: v8::GCCallbackFlags,
|
|
71
|
+
_data: *mut c_void,
|
|
72
|
+
) {
|
|
73
|
+
let checked = std::panic::catch_unwind(move || {
|
|
74
|
+
// SAFETY: V8 calls a GC epilogue callback on the thread that has the
|
|
75
|
+
// isolate entered, with its own non-null pointer to it.
|
|
76
|
+
let isolate = unsafe { v8::Isolate::ref_from_raw_isolate_ptr_mut(&mut isolate) };
|
|
77
|
+
crate::runner::check_heap(isolate);
|
|
78
|
+
});
|
|
79
|
+
if checked.is_err() {
|
|
80
|
+
std::process::abort();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Asks the isolate's runner to park for a pending fork the next time V8
|
|
85
|
+
/// checks for interrupts, where JavaScript may be running for a long time.
|
|
86
|
+
pub fn interrupt_for_fork(handle: &v8::IsolateHandle) {
|
|
87
|
+
handle.request_interrupt(park_for_fork, std::ptr::null_mut());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
extern "C" fn park_for_fork(isolate: v8::UnsafeRawIsolatePtr, _data: *mut std::ffi::c_void) {
|
|
91
|
+
let parked = std::panic::catch_unwind(|| {
|
|
92
|
+
// SAFETY: V8 calls an interrupt callback on the thread that has the
|
|
93
|
+
// isolate entered, with its own non-null pointer to it.
|
|
94
|
+
let isolate = unsafe { v8::Isolate::ref_from_raw_isolate_ptr(&isolate) };
|
|
95
|
+
crate::runner::park_for_fork(isolate);
|
|
96
|
+
});
|
|
97
|
+
if parked.is_err() {
|
|
98
|
+
std::process::abort();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Returns the free pages of every glibc malloc arena in the process to the
|
|
103
|
+
/// system. glibc keeps what is freed for later allocations, and parsing a
|
|
104
|
+
/// large script frees most of what it took: about half of a loaded bundle's
|
|
105
|
+
/// resident memory, which Ruby's collector never hands back.
|
|
106
|
+
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
|
107
|
+
pub fn release_free_memory() {
|
|
108
|
+
// SAFETY: malloc_trim only reads and updates glibc's allocator state,
|
|
109
|
+
// under the locks glibc takes for it.
|
|
110
|
+
let _ = unsafe { libc::malloc_trim(0) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// A no-op where malloc is not glibc's.
|
|
114
|
+
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
|
|
115
|
+
pub const fn release_free_memory() {}
|
|
116
|
+
|
|
117
|
+
// Slots of v8::Platform's vtable, in v8-platform.h's declaration order after
|
|
118
|
+
// the destructor's two; the v8-bump skill checks them against each new V8.
|
|
119
|
+
const NUMBER_OF_WORKER_THREADS: usize = 6;
|
|
120
|
+
const POST_TASK_ON_WORKER_THREAD_IMPL: usize = 20;
|
|
121
|
+
const POST_DELAYED_TASK_ON_WORKER_THREAD_IMPL: usize = 21;
|
|
122
|
+
// The words before the first slot: the offset to the top and the type info.
|
|
123
|
+
const VTABLE_PREFIX: usize = 2;
|
|
124
|
+
|
|
125
|
+
/// `rusty_v8`'s `CustomPlatform` vtable as words, the two before its first slot
|
|
126
|
+
/// included; `nm -S` sizes it at 0xc0 bytes.
|
|
127
|
+
pub type PlatformVtable = [usize; VTABLE_PREFIX + 22];
|
|
128
|
+
|
|
129
|
+
/// A copy of `platform`'s vtable whose worker thread entries lead to
|
|
130
|
+
/// `process::workers`, which V8 then also sizes its jobs by.
|
|
131
|
+
#[must_use]
|
|
132
|
+
pub fn worker_vtable(platform: &v8::Platform) -> Box<PlatformVtable> {
|
|
133
|
+
let this: *const v8::Platform = platform;
|
|
134
|
+
// SAFETY: `platform` is a live C++ v8::Platform, whose first word points
|
|
135
|
+
// at its vtable's first slot.
|
|
136
|
+
let first_slot = unsafe { *this.cast::<*const usize>() };
|
|
137
|
+
// SAFETY: a vtable keeps the offset to the top and the type info right
|
|
138
|
+
// before the slot its objects point at.
|
|
139
|
+
let start = unsafe { first_slot.sub(VTABLE_PREFIX) };
|
|
140
|
+
// SAFETY: the vtable is a whole `PlatformVtable` of initialized words
|
|
141
|
+
// from `start`, never written once the library is loaded.
|
|
142
|
+
let mut vtable = Box::new(unsafe { start.cast::<PlatformVtable>().read() });
|
|
143
|
+
let entries = [
|
|
144
|
+
(
|
|
145
|
+
NUMBER_OF_WORKER_THREADS,
|
|
146
|
+
number_of_worker_threads as *const (),
|
|
147
|
+
),
|
|
148
|
+
(
|
|
149
|
+
POST_TASK_ON_WORKER_THREAD_IMPL,
|
|
150
|
+
post_task_on_worker_thread as *const (),
|
|
151
|
+
),
|
|
152
|
+
(
|
|
153
|
+
POST_DELAYED_TASK_ON_WORKER_THREAD_IMPL,
|
|
154
|
+
post_delayed_task_on_worker_thread as *const (),
|
|
155
|
+
),
|
|
156
|
+
];
|
|
157
|
+
for (slot, entry) in entries {
|
|
158
|
+
vtable[VTABLE_PREFIX + slot] = entry.addr();
|
|
159
|
+
}
|
|
160
|
+
vtable
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Has every later virtual call on `platform` go through `vtable`.
|
|
164
|
+
pub fn use_vtable(platform: &mut v8::Platform, vtable: &'static PlatformVtable) {
|
|
165
|
+
let this: *mut v8::Platform = platform;
|
|
166
|
+
let first_slot = vtable[VTABLE_PREFIX..].as_ptr();
|
|
167
|
+
// SAFETY: `platform` is a live C++ object whose first word is its vtable
|
|
168
|
+
// pointer, not yet handed to V8, and `vtable` lives as long as the
|
|
169
|
+
// process with the platform's own entries in every other slot.
|
|
170
|
+
unsafe { this.cast::<*const usize>().write(first_slot) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
extern "C" fn number_of_worker_threads(_platform: *const c_void) -> libc::c_int {
|
|
174
|
+
let threads = std::panic::catch_unwind(crate::process::workers::threads);
|
|
175
|
+
threads.map_or_else(
|
|
176
|
+
|_| std::process::abort(),
|
|
177
|
+
|threads| libc::c_int::try_from(threads).unwrap_or(libc::c_int::MAX),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// V8's libc++ passes a std::unique_ptr<Task> in a register, and the callee
|
|
182
|
+
// owns the task from then on. The priority is ignored, as rusty_v8's platform
|
|
183
|
+
// ignores it in V8's default `PriorityMode::kDontApply`.
|
|
184
|
+
extern "C" fn post_task_on_worker_thread(
|
|
185
|
+
_platform: *const c_void,
|
|
186
|
+
_priority: u8,
|
|
187
|
+
task: *mut c_void,
|
|
188
|
+
_location: *const c_void,
|
|
189
|
+
) {
|
|
190
|
+
if std::panic::catch_unwind(|| post_worker_task(task, Duration::ZERO)).is_err() {
|
|
191
|
+
std::process::abort();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
extern "C" fn post_delayed_task_on_worker_thread(
|
|
196
|
+
_platform: *const c_void,
|
|
197
|
+
_priority: u8,
|
|
198
|
+
task: *mut c_void,
|
|
199
|
+
delay_in_seconds: f64,
|
|
200
|
+
_location: *const c_void,
|
|
201
|
+
) {
|
|
202
|
+
let posted = std::panic::catch_unwind(|| {
|
|
203
|
+
let delay = Duration::try_from_secs_f64(delay_in_seconds.max(0.0)).unwrap_or(Duration::MAX);
|
|
204
|
+
post_worker_task(task, delay);
|
|
205
|
+
});
|
|
206
|
+
if posted.is_err() {
|
|
207
|
+
std::process::abort();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn post_worker_task(task: *mut c_void, delay: Duration) {
|
|
212
|
+
if let Some(task) = NonNull::new(task) {
|
|
213
|
+
crate::process::workers::post(WorkerTask(task), delay);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// rusty_v8's own entry points for a task V8 handed over, which it also runs
|
|
218
|
+
// the runners' foreground tasks with.
|
|
219
|
+
unsafe extern "C" {
|
|
220
|
+
fn v8__Task__Run(task: *mut c_void);
|
|
221
|
+
fn v8__Task__DELETE(task: *mut c_void);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// A `v8::Task` posted for a worker thread, owned from the post on: run once,
|
|
225
|
+
/// then deleted.
|
|
226
|
+
pub struct WorkerTask(NonNull<c_void>);
|
|
227
|
+
|
|
228
|
+
// SAFETY: V8 posts worker tasks from any thread for any worker thread to run.
|
|
229
|
+
unsafe impl Send for WorkerTask {}
|
|
230
|
+
|
|
231
|
+
impl WorkerTask {
|
|
232
|
+
pub fn run(self) {
|
|
233
|
+
// SAFETY: the task is V8's, whole and not yet run.
|
|
234
|
+
unsafe { v8__Task__Run(self.0.as_ptr()) };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
impl Drop for WorkerTask {
|
|
239
|
+
fn drop(&mut self) {
|
|
240
|
+
// SAFETY: the task is V8's and deleted once, here.
|
|
241
|
+
unsafe { v8__Task__DELETE(self.0.as_ptr()) };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// The processors online, which `rusty_v8` sizes a platform's default pool by.
|
|
246
|
+
#[must_use]
|
|
247
|
+
pub fn processors_online() -> usize {
|
|
248
|
+
// SAFETY: sysconf only reads the system's configuration.
|
|
249
|
+
let online = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) };
|
|
250
|
+
usize::try_from(online).unwrap_or(0)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Runs the engine's fork handlers inside every `fork()` of the process.
|
|
254
|
+
pub fn register_fork_handlers() {
|
|
255
|
+
// SAFETY: the handlers are plain functions that live as long as the
|
|
256
|
+
// process and never unwind.
|
|
257
|
+
let registered =
|
|
258
|
+
unsafe { libc::pthread_atfork(Some(on_prepare), Some(on_parent), Some(on_child)) };
|
|
259
|
+
debug_assert_eq!(registered, 0, "pthread_atfork");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
extern "C" fn on_prepare() {
|
|
263
|
+
if std::panic::catch_unwind(crate::process::fork::prepare).is_err() {
|
|
264
|
+
std::process::abort();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
extern "C" fn on_parent() {
|
|
269
|
+
if std::panic::catch_unwind(crate::process::fork::parent).is_err() {
|
|
270
|
+
std::process::abort();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
extern "C" fn on_child() {
|
|
275
|
+
if std::panic::catch_unwind(crate::process::fork::child).is_err() {
|
|
276
|
+
std::process::abort();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// `v8::Task`s made in Rust, and the platform's worker thread entries called
|
|
281
|
+
/// the way V8 calls them.
|
|
282
|
+
#[cfg(test)]
|
|
283
|
+
pub mod testing {
|
|
284
|
+
use std::ffi::c_void;
|
|
285
|
+
use std::ptr::NonNull;
|
|
286
|
+
|
|
287
|
+
use super::{
|
|
288
|
+
NUMBER_OF_WORKER_THREADS, POST_DELAYED_TASK_ON_WORKER_THREAD_IMPL,
|
|
289
|
+
POST_TASK_ON_WORKER_THREAD_IMPL, WorkerTask,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// v8::TaskPriority::kUserBlocking.
|
|
293
|
+
const USER_BLOCKING: u8 = 2;
|
|
294
|
+
|
|
295
|
+
type TaskMethod = unsafe extern "C" fn(*mut RustTask);
|
|
296
|
+
// Apple's arm64 ABI has a complete destructor return `this`; elsewhere
|
|
297
|
+
// the returned value is ignored.
|
|
298
|
+
type TaskDestructor = unsafe extern "C" fn(*mut RustTask) -> *mut RustTask;
|
|
299
|
+
|
|
300
|
+
// A C++ vtable as clang lays one out for v8::Task: the object points past
|
|
301
|
+
// the first two words, at the destructors and then `Run`.
|
|
302
|
+
#[repr(C)]
|
|
303
|
+
struct TaskVtable {
|
|
304
|
+
offset_to_top: isize,
|
|
305
|
+
type_info: *const c_void,
|
|
306
|
+
complete_destructor: TaskDestructor,
|
|
307
|
+
deleting_destructor: TaskMethod,
|
|
308
|
+
run: TaskMethod,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// A constant reference, promoted to memory that lives as long as the
|
|
312
|
+
// program, as a vtable must.
|
|
313
|
+
const TASK_VTABLE: &TaskVtable = &TaskVtable {
|
|
314
|
+
offset_to_top: 0,
|
|
315
|
+
type_info: std::ptr::null(),
|
|
316
|
+
complete_destructor: destroy_task,
|
|
317
|
+
deleting_destructor: delete_task,
|
|
318
|
+
run: run_task,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
#[repr(C)]
|
|
322
|
+
struct RustTask {
|
|
323
|
+
vtable: *const TaskMethod,
|
|
324
|
+
run: Option<Box<dyn FnOnce() + Send>>,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
unsafe extern "C" fn destroy_task(task: *mut RustTask) -> *mut RustTask {
|
|
328
|
+
let destroyed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
329
|
+
// SAFETY: the task's owner destroys it once, and it was built in
|
|
330
|
+
// `task`.
|
|
331
|
+
unsafe { std::ptr::drop_in_place(task) };
|
|
332
|
+
}));
|
|
333
|
+
if destroyed.is_err() {
|
|
334
|
+
std::process::abort();
|
|
335
|
+
}
|
|
336
|
+
task
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
unsafe extern "C" fn delete_task(task: *mut RustTask) {
|
|
340
|
+
let deleted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
341
|
+
// SAFETY: the task's owner deletes it once; it came from
|
|
342
|
+
// `Box::into_raw`.
|
|
343
|
+
drop(unsafe { Box::from_raw(task) });
|
|
344
|
+
}));
|
|
345
|
+
if deleted.is_err() {
|
|
346
|
+
std::process::abort();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
unsafe extern "C" fn run_task(task: *mut RustTask) {
|
|
351
|
+
let ran = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
352
|
+
// SAFETY: the task's owner runs it on one thread, before deleting
|
|
353
|
+
// it.
|
|
354
|
+
let run = unsafe { (*task).run.take() };
|
|
355
|
+
if let Some(run) = run {
|
|
356
|
+
run();
|
|
357
|
+
}
|
|
358
|
+
}));
|
|
359
|
+
if ran.is_err() {
|
|
360
|
+
std::process::abort();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn task(run: Box<dyn FnOnce() + Send>) -> NonNull<RustTask> {
|
|
365
|
+
NonNull::from(Box::leak(Box::new(RustTask {
|
|
366
|
+
vtable: (&raw const TASK_VTABLE.complete_destructor).cast(),
|
|
367
|
+
run: Some(run),
|
|
368
|
+
})))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// A worker task that runs `run`, as if V8 had posted it.
|
|
372
|
+
pub fn worker_task(run: Box<dyn FnOnce() + Send>) -> WorkerTask {
|
|
373
|
+
WorkerTask(task(run).cast())
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const fn platform_method(platform: &v8::Platform, slot: usize) -> *const c_void {
|
|
377
|
+
let this: *const v8::Platform = platform;
|
|
378
|
+
// SAFETY: `platform` is a live C++ v8::Platform, whose first word
|
|
379
|
+
// points at its vtable.
|
|
380
|
+
let vtable = unsafe { *this.cast::<*const *const c_void>() };
|
|
381
|
+
// SAFETY: v8::Platform declares more virtual functions than `slot`,
|
|
382
|
+
// so the entry is inside the vtable.
|
|
383
|
+
let entry = unsafe { vtable.add(slot) };
|
|
384
|
+
// SAFETY: the entry is an initialized function pointer of the vtable.
|
|
385
|
+
unsafe { *entry }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// The platform's `NumberOfWorkerThreads()`.
|
|
389
|
+
pub fn worker_threads(platform: &v8::Platform) -> usize {
|
|
390
|
+
let method = platform_method(platform, NUMBER_OF_WORKER_THREADS);
|
|
391
|
+
// SAFETY: the slot holds `int Platform::NumberOfWorkerThreads()`,
|
|
392
|
+
// called with `this` first, as clang passes it.
|
|
393
|
+
let method = unsafe {
|
|
394
|
+
std::mem::transmute::<
|
|
395
|
+
*const c_void,
|
|
396
|
+
unsafe extern "C" fn(*const v8::Platform) -> libc::c_int,
|
|
397
|
+
>(method)
|
|
398
|
+
};
|
|
399
|
+
// SAFETY: `platform` is the live object the method belongs to.
|
|
400
|
+
let count = unsafe { method(platform) };
|
|
401
|
+
usize::try_from(count).unwrap_or(0)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// Posts `run` through the platform's `PostTaskOnWorkerThreadImpl`, as
|
|
405
|
+
/// V8's jobs do.
|
|
406
|
+
pub fn post_on_worker_thread(platform: &v8::Platform, run: Box<dyn FnOnce() + Send>) {
|
|
407
|
+
// v8::SourceLocation holds one pointer, which V8 reads as no location
|
|
408
|
+
// when null.
|
|
409
|
+
let location: *const c_void = std::ptr::null();
|
|
410
|
+
let method = platform_method(platform, POST_TASK_ON_WORKER_THREAD_IMPL);
|
|
411
|
+
// SAFETY: the slot holds `void Platform::PostTaskOnWorkerThreadImpl(
|
|
412
|
+
// TaskPriority, std::unique_ptr<Task>, const SourceLocation&)`. V8's
|
|
413
|
+
// libc++ passes a unique_ptr in a register, as its own callers do, and
|
|
414
|
+
// the callee owns it from then on.
|
|
415
|
+
let method = unsafe {
|
|
416
|
+
std::mem::transmute::<
|
|
417
|
+
*const c_void,
|
|
418
|
+
unsafe extern "C" fn(*const v8::Platform, u8, *mut RustTask, *const *const c_void),
|
|
419
|
+
>(method)
|
|
420
|
+
};
|
|
421
|
+
let task = task(run).as_ptr();
|
|
422
|
+
// SAFETY: `platform` is the live object the method belongs to, the
|
|
423
|
+
// task is a whole v8::Task, and `location` outlives the call.
|
|
424
|
+
unsafe { method(platform, USER_BLOCKING, task, &raw const location) };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// Posts `run` through the platform's `PostDelayedTaskOnWorkerThreadImpl`
|
|
428
|
+
/// to run after `delay` seconds.
|
|
429
|
+
pub fn post_delayed_on_worker_thread(
|
|
430
|
+
platform: &v8::Platform,
|
|
431
|
+
delay: f64,
|
|
432
|
+
run: Box<dyn FnOnce() + Send>,
|
|
433
|
+
) {
|
|
434
|
+
let location: *const c_void = std::ptr::null();
|
|
435
|
+
let method = platform_method(platform, POST_DELAYED_TASK_ON_WORKER_THREAD_IMPL);
|
|
436
|
+
// SAFETY: the slot holds `void PostDelayedTaskOnWorkerThreadImpl(
|
|
437
|
+
// TaskPriority, std::unique_ptr<Task>, double, const SourceLocation&)`,
|
|
438
|
+
// the unique_ptr passed as in `post_on_worker_thread`.
|
|
439
|
+
let method = unsafe {
|
|
440
|
+
std::mem::transmute::<
|
|
441
|
+
*const c_void,
|
|
442
|
+
unsafe extern "C" fn(
|
|
443
|
+
*const v8::Platform,
|
|
444
|
+
u8,
|
|
445
|
+
*mut RustTask,
|
|
446
|
+
f64,
|
|
447
|
+
*const *const c_void,
|
|
448
|
+
),
|
|
449
|
+
>(method)
|
|
450
|
+
};
|
|
451
|
+
let task = task(run).as_ptr();
|
|
452
|
+
// SAFETY: as in `post_on_worker_thread`.
|
|
453
|
+
unsafe { method(platform, USER_BLOCKING, task, delay, &raw const location) };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#[cfg(test)]
|
|
458
|
+
mod tests {
|
|
459
|
+
use std::collections::HashSet;
|
|
460
|
+
use std::sync::mpsc;
|
|
461
|
+
use std::sync::{Arc, Condvar, Mutex};
|
|
462
|
+
use std::time::{Duration, Instant};
|
|
463
|
+
|
|
464
|
+
use super::testing::{post_delayed_on_worker_thread, post_on_worker_thread, worker_threads};
|
|
465
|
+
|
|
466
|
+
// The slots are V8's own worker thread entries in the V8 this crate
|
|
467
|
+
// links: a platform left as V8 builds it reports the pool it was asked
|
|
468
|
+
// for and runs tasks posted through them on its own threads.
|
|
469
|
+
#[test]
|
|
470
|
+
fn the_worker_slots_are_v8s_own() {
|
|
471
|
+
let platform = v8::new_custom_platform(3, false, true, crate::process::tasks::Foreground);
|
|
472
|
+
assert_eq!(worker_threads(&platform), 3);
|
|
473
|
+
let (ran, order) = mpsc::channel();
|
|
474
|
+
let record = |label: &'static str| -> Box<dyn FnOnce() + Send> {
|
|
475
|
+
let ran = ran.clone();
|
|
476
|
+
Box::new(move || {
|
|
477
|
+
let pool = std::thread::current().name() == Some("mini_racer_rs worker");
|
|
478
|
+
ran.send((label, pool, Instant::now())).expect("receiver");
|
|
479
|
+
})
|
|
480
|
+
};
|
|
481
|
+
let posted = Instant::now();
|
|
482
|
+
post_delayed_on_worker_thread(&platform, 0.05, record("delayed"));
|
|
483
|
+
post_on_worker_thread(&platform, record("now"));
|
|
484
|
+
let first = order.recv_timeout(Duration::from_secs(10)).expect("a task");
|
|
485
|
+
let second = order.recv_timeout(Duration::from_secs(10)).expect("a task");
|
|
486
|
+
assert_eq!((first.0, first.1), ("now", false));
|
|
487
|
+
assert_eq!((second.0, second.1), ("delayed", false));
|
|
488
|
+
assert!(second.2 - posted >= Duration::from_millis(50));
|
|
489
|
+
drop(platform);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Patched, the platform reports this process's pool, and tasks posted
|
|
493
|
+
// through it run on every pool thread, each on its own, and are deleted.
|
|
494
|
+
// Each waits for the rest to start, so one thread cannot run them all.
|
|
495
|
+
#[test]
|
|
496
|
+
fn runs_a_task_on_each_worker_thread() {
|
|
497
|
+
let _turn = mini_racer_sync::lock(&crate::process::workers::TESTS);
|
|
498
|
+
crate::process::ensure_initialized();
|
|
499
|
+
let platform = v8::V8::get_current_platform();
|
|
500
|
+
let threads = worker_threads(&platform);
|
|
501
|
+
assert_eq!(threads, crate::process::workers::threads());
|
|
502
|
+
assert_eq!(threads, super::processors_online().clamp(1, 16));
|
|
503
|
+
let started = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
|
|
504
|
+
for _ in 0..threads {
|
|
505
|
+
let started = Arc::clone(&started);
|
|
506
|
+
post_on_worker_thread(
|
|
507
|
+
&platform,
|
|
508
|
+
Box::new(move || {
|
|
509
|
+
let (seen, all) = &*started;
|
|
510
|
+
let mut seen = seen.lock().expect("lock");
|
|
511
|
+
let thread = std::thread::current();
|
|
512
|
+
seen.push((thread.id(), thread.name().map(str::to_owned)));
|
|
513
|
+
all.notify_all();
|
|
514
|
+
let deadline = Instant::now() + Duration::from_secs(10);
|
|
515
|
+
while seen.len() < threads && Instant::now() < deadline {
|
|
516
|
+
seen = all
|
|
517
|
+
.wait_timeout(seen, Duration::from_millis(10))
|
|
518
|
+
.expect("wait")
|
|
519
|
+
.0;
|
|
520
|
+
}
|
|
521
|
+
drop(seen);
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
let deadline = Instant::now() + Duration::from_secs(10);
|
|
526
|
+
while Arc::strong_count(&started) > 1 && Instant::now() < deadline {
|
|
527
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
528
|
+
}
|
|
529
|
+
let seen = std::mem::take(&mut *started.0.lock().expect("lock"));
|
|
530
|
+
assert_eq!(seen.len(), threads);
|
|
531
|
+
assert_eq!(
|
|
532
|
+
seen.iter().map(|(id, _)| id).collect::<HashSet<_>>().len(),
|
|
533
|
+
threads
|
|
534
|
+
);
|
|
535
|
+
assert!(
|
|
536
|
+
seen.iter()
|
|
537
|
+
.all(|(_, name)| name.as_deref() == Some("mini_racer_rs worker"))
|
|
538
|
+
);
|
|
539
|
+
assert_eq!(Arc::strong_count(&started), 1);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#[test]
|
|
543
|
+
fn runs_delayed_tasks_by_deadline_and_not_before_their_delay() {
|
|
544
|
+
let _turn = mini_racer_sync::lock(&crate::process::workers::TESTS);
|
|
545
|
+
crate::process::ensure_initialized();
|
|
546
|
+
let platform = v8::V8::get_current_platform();
|
|
547
|
+
let ran = Arc::new(Mutex::new(Vec::new()));
|
|
548
|
+
let record = |label: &'static str| -> Box<dyn FnOnce() + Send> {
|
|
549
|
+
let ran = Arc::clone(&ran);
|
|
550
|
+
Box::new(move || ran.lock().expect("lock").push((label, Instant::now())))
|
|
551
|
+
};
|
|
552
|
+
let posted = Instant::now();
|
|
553
|
+
post_delayed_on_worker_thread(&platform, 0.12, record("later"));
|
|
554
|
+
post_delayed_on_worker_thread(&platform, 0.06, record("sooner"));
|
|
555
|
+
post_on_worker_thread(&platform, record("now"));
|
|
556
|
+
let deadline = Instant::now() + Duration::from_secs(10);
|
|
557
|
+
while ran.lock().expect("lock").len() < 3 && Instant::now() < deadline {
|
|
558
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
559
|
+
}
|
|
560
|
+
let ran = std::mem::take(&mut *ran.lock().expect("lock"));
|
|
561
|
+
let labels: Vec<_> = ran.iter().map(|(label, _)| *label).collect();
|
|
562
|
+
assert_eq!(labels, ["now", "sooner", "later"]);
|
|
563
|
+
assert!(ran[1].1 - posted >= Duration::from_millis(60));
|
|
564
|
+
assert!(ran[2].1 - posted >= Duration::from_millis(120));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
description = "Concurrency protocol types for mini_racer_rs"
|
|
3
|
+
edition.workspace = true
|
|
4
|
+
license.workspace = true
|
|
5
|
+
name = "mini_racer_sync"
|
|
6
|
+
publish.workspace = true
|
|
7
|
+
repository.workspace = true
|
|
8
|
+
rust-version.workspace = true
|
|
9
|
+
version.workspace = true
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
thiserror.workspace = true
|
|
13
|
+
|
|
14
|
+
[target.'cfg(loom)'.dependencies]
|
|
15
|
+
loom.workspace = true
|
|
16
|
+
|
|
17
|
+
[lints]
|
|
18
|
+
workspace = true
|