Skip to content

Commit 778b159

Browse files
fraidevbartlomieju
andauthored
feat: add PlatformImpl trait for custom platform callbacks (#1924)
Add a trait-based API (following the V8InspectorClientImpl pattern) that lets embedders hook into platform virtual methods. A CustomPlatform C++ class subclasses DefaultPlatform and delegates on_foreground_task_posted and on_isolate_shutdown to a Rust PlatformImpl trait object. This allows embedders like Deno to receive notifications when V8 background threads post foreground tasks, enabling event-driven wakeups instead of polling. --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
1 parent f1f38ba commit 778b159

File tree

4 files changed

+443
-0
lines changed

4 files changed

+443
-0
lines changed

src/binding.cc

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include <cstddef>
55
#include <cstdint>
66
#include <cstdio>
7+
#include <map>
8+
#include <mutex>
79
#include <thread>
810

911
#include "cppgc/allocation.h"
@@ -3010,6 +3012,144 @@ v8::StartupData v8__SnapshotCreator__CreateBlob(
30103012
return self->CreateBlob(function_code_handling);
30113013
}
30123014

3015+
// Rust-side callbacks for trait-based CustomPlatform (PlatformImpl trait).
3016+
// Each callback corresponds to a C++ virtual method on TaskRunner or Platform.
3017+
// `context` is a pointer to the Rust Box<dyn PlatformImpl>.
3018+
extern "C" {
3019+
void v8__Platform__CustomPlatform__BASE__PostTask(void* context, void* isolate);
3020+
void v8__Platform__CustomPlatform__BASE__PostNonNestableTask(void* context,
3021+
void* isolate);
3022+
void v8__Platform__CustomPlatform__BASE__PostDelayedTask(
3023+
void* context, void* isolate, double delay_in_seconds);
3024+
void v8__Platform__CustomPlatform__BASE__PostNonNestableDelayedTask(
3025+
void* context, void* isolate, double delay_in_seconds);
3026+
void v8__Platform__CustomPlatform__BASE__PostIdleTask(void* context,
3027+
void* isolate);
3028+
void v8__Platform__CustomPlatform__BASE__DROP(void* context);
3029+
}
3030+
3031+
// TaskRunner wrapper that intercepts all PostTask* virtual methods, forwards
3032+
// tasks to the default platform's queue, and notifies Rust via the
3033+
// corresponding PlatformImpl trait method.
3034+
class CustomTaskRunner final : public v8::TaskRunner {
3035+
public:
3036+
CustomTaskRunner(std::shared_ptr<v8::TaskRunner> wrapped, void* context,
3037+
v8::Isolate* isolate)
3038+
: wrapped_(std::move(wrapped)), context_(context), isolate_(isolate) {}
3039+
3040+
bool IdleTasksEnabled() override { return wrapped_->IdleTasksEnabled(); }
3041+
bool NonNestableTasksEnabled() const override {
3042+
return wrapped_->NonNestableTasksEnabled();
3043+
}
3044+
bool NonNestableDelayedTasksEnabled() const override {
3045+
return wrapped_->NonNestableDelayedTasksEnabled();
3046+
}
3047+
3048+
protected:
3049+
void PostTaskImpl(std::unique_ptr<v8::Task> task,
3050+
const v8::SourceLocation& location) override {
3051+
wrapped_->PostTask(std::move(task), location);
3052+
v8__Platform__CustomPlatform__BASE__PostTask(context_,
3053+
static_cast<void*>(isolate_));
3054+
}
3055+
void PostNonNestableTaskImpl(std::unique_ptr<v8::Task> task,
3056+
const v8::SourceLocation& location) override {
3057+
wrapped_->PostNonNestableTask(std::move(task), location);
3058+
v8__Platform__CustomPlatform__BASE__PostNonNestableTask(
3059+
context_, static_cast<void*>(isolate_));
3060+
}
3061+
void PostDelayedTaskImpl(std::unique_ptr<v8::Task> task,
3062+
double delay_in_seconds,
3063+
const v8::SourceLocation& location) override {
3064+
wrapped_->PostDelayedTask(std::move(task), delay_in_seconds, location);
3065+
v8__Platform__CustomPlatform__BASE__PostDelayedTask(
3066+
context_, static_cast<void*>(isolate_),
3067+
delay_in_seconds > 0 ? delay_in_seconds : 0.0);
3068+
}
3069+
void PostNonNestableDelayedTaskImpl(
3070+
std::unique_ptr<v8::Task> task, double delay_in_seconds,
3071+
const v8::SourceLocation& location) override {
3072+
wrapped_->PostNonNestableDelayedTask(std::move(task), delay_in_seconds,
3073+
location);
3074+
v8__Platform__CustomPlatform__BASE__PostNonNestableDelayedTask(
3075+
context_, static_cast<void*>(isolate_),
3076+
delay_in_seconds > 0 ? delay_in_seconds : 0.0);
3077+
}
3078+
void PostIdleTaskImpl(std::unique_ptr<v8::IdleTask> task,
3079+
const v8::SourceLocation& location) override {
3080+
wrapped_->PostIdleTask(std::move(task), location);
3081+
v8__Platform__CustomPlatform__BASE__PostIdleTask(
3082+
context_, static_cast<void*>(isolate_));
3083+
}
3084+
3085+
private:
3086+
std::shared_ptr<v8::TaskRunner> wrapped_;
3087+
void* context_;
3088+
v8::Isolate* isolate_;
3089+
};
3090+
3091+
// Platform subclass that wraps each isolate's TaskRunner to notify Rust
3092+
// when foreground tasks are posted. Follows the inspector API pattern.
3093+
//
3094+
// NotifyIsolateShutdown is NOT intercepted here because it is not virtual
3095+
// on DefaultPlatform — V8's free function does static_cast<DefaultPlatform*>
3096+
// and calls it directly, bypassing any override. Isolate cleanup must be
3097+
// handled on the Rust side (e.g. in the isolate's Drop impl).
3098+
class CustomPlatform : public v8::platform::DefaultPlatform {
3099+
using IdleTaskSupport = v8::platform::IdleTaskSupport;
3100+
3101+
public:
3102+
CustomPlatform(int thread_pool_size, IdleTaskSupport idle_task_support,
3103+
bool unprotected, void* context)
3104+
: DefaultPlatform(thread_pool_size, idle_task_support),
3105+
unprotected_(unprotected),
3106+
context_(context) {}
3107+
3108+
// SAFETY: The platform is single-owner (via unique_ptr). The destructor
3109+
// runs after all isolates have been disposed and no more task runner
3110+
// callbacks can fire, so DROP does not race with other callbacks.
3111+
~CustomPlatform() override {
3112+
v8__Platform__CustomPlatform__BASE__DROP(context_);
3113+
}
3114+
3115+
std::shared_ptr<v8::TaskRunner> GetForegroundTaskRunner(
3116+
v8::Isolate* isolate, v8::TaskPriority priority) override {
3117+
auto original = DefaultPlatform::GetForegroundTaskRunner(isolate, priority);
3118+
std::lock_guard<std::mutex> lock(mutex_);
3119+
auto key = std::make_pair(isolate, priority);
3120+
auto it = runners_.find(key);
3121+
if (it != runners_.end()) {
3122+
auto runner = it->second.lock();
3123+
if (runner) return runner;
3124+
}
3125+
auto custom =
3126+
std::make_shared<CustomTaskRunner>(original, context_, isolate);
3127+
runners_[key] = custom;
3128+
return custom;
3129+
}
3130+
3131+
// When unprotected, disable thread-isolated allocations (same as
3132+
// UnprotectedDefaultPlatform). Required when isolates may be created on
3133+
// threads other than the one that called v8::V8::Initialize (e.g. worker
3134+
// threads in Deno).
3135+
v8::ThreadIsolatedAllocator* GetThreadIsolatedAllocator() override {
3136+
if (unprotected_) return nullptr;
3137+
return DefaultPlatform::GetThreadIsolatedAllocator();
3138+
}
3139+
3140+
private:
3141+
bool unprotected_;
3142+
void* context_;
3143+
std::mutex mutex_;
3144+
// weak_ptr so runners are kept alive only while V8 holds a reference.
3145+
// When V8 drops its shared_ptr (e.g. on isolate shutdown), the weak_ptr
3146+
// expires and a fresh wrapper is created if GetForegroundTaskRunner is
3147+
// called again. This avoids preventing cleanup of the underlying runner.
3148+
std::map<std::pair<v8::Isolate*, v8::TaskPriority>,
3149+
std::weak_ptr<CustomTaskRunner>>
3150+
runners_;
3151+
};
3152+
30133153
class UnprotectedDefaultPlatform : public v8::platform::DefaultPlatform {
30143154
using IdleTaskSupport = v8::platform::IdleTaskSupport;
30153155
using InProcessStackDumping = v8::platform::InProcessStackDumping;
@@ -3063,6 +3203,21 @@ v8::Platform* v8__Platform__NewSingleThreadedDefaultPlatform(
30633203
.release();
30643204
}
30653205

3206+
v8::Platform* v8__Platform__NewCustomPlatform(int thread_pool_size,
3207+
bool idle_task_support,
3208+
bool unprotected, void* context) {
3209+
if (thread_pool_size < 1) {
3210+
thread_pool_size = std::thread::hardware_concurrency();
3211+
}
3212+
thread_pool_size = std::max(std::min(thread_pool_size, 16), 1);
3213+
return std::make_unique<CustomPlatform>(
3214+
thread_pool_size,
3215+
idle_task_support ? v8::platform::IdleTaskSupport::kEnabled
3216+
: v8::platform::IdleTaskSupport::kDisabled,
3217+
unprotected, context)
3218+
.release();
3219+
}
3220+
30663221
bool v8__Platform__PumpMessageLoop(v8::Platform* platform, v8::Isolate* isolate,
30673222
bool wait_for_work) {
30683223
return v8::platform::PumpMessageLoop(

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ pub use microtask::MicrotaskQueue;
137137
pub use module::*;
138138
pub use object::*;
139139
pub use platform::Platform;
140+
pub use platform::PlatformImpl;
141+
pub use platform::new_custom_platform;
140142
pub use platform::new_default_platform;
141143
pub use platform::new_single_threaded_default_platform;
142144
pub use platform::new_unprotected_default_platform;

src/platform.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ unsafe extern "C" {
2222
fn v8__Platform__NewSingleThreadedDefaultPlatform(
2323
idle_task_support: bool,
2424
) -> *mut Platform;
25+
fn v8__Platform__NewCustomPlatform(
26+
thread_pool_size: int,
27+
idle_task_support: bool,
28+
unprotected: bool,
29+
context: *mut std::ffi::c_void,
30+
) -> *mut Platform;
2531
fn v8__Platform__DELETE(this: *mut Platform);
2632

2733
fn v8__Platform__PumpMessageLoop(
@@ -60,6 +66,129 @@ unsafe extern "C" {
6066
#[derive(Debug)]
6167
pub struct Platform(Opaque);
6268

69+
/// Trait for customizing platform behavior, following the same pattern as
70+
/// [`V8InspectorClientImpl`](crate::inspector::V8InspectorClientImpl).
71+
///
72+
/// Implement this trait to receive callbacks for overridden C++ virtual
73+
/// methods on the `DefaultPlatform` and its per-isolate `TaskRunner`.
74+
///
75+
/// The C++ `CustomPlatform` wraps each isolate's `TaskRunner` so that
76+
/// every `PostTask` / `PostDelayedTask` / etc. call is forwarded to the
77+
/// default implementation *and* notifies Rust through the corresponding
78+
/// trait method.
79+
///
80+
/// All methods have default no-op implementations; override only what
81+
/// you need.
82+
///
83+
/// Implementations must be `Send + Sync` as callbacks may fire from any
84+
/// thread.
85+
#[allow(unused_variables)]
86+
pub trait PlatformImpl: Send + Sync {
87+
// ---- TaskRunner virtual methods ----
88+
89+
/// Called when `TaskRunner::PostTask` is invoked for the given isolate.
90+
///
91+
/// The task itself has already been forwarded to the default platform's
92+
/// queue and will be executed by `PumpMessageLoop`. This callback is a
93+
/// notification that a new task is available.
94+
///
95+
/// May be called from ANY thread (V8 background threads, etc.).
96+
fn post_task(&self, isolate_ptr: *mut std::ffi::c_void) {}
97+
98+
/// Called when `TaskRunner::PostNonNestableTask` is invoked.
99+
///
100+
/// Same semantics as [`post_task`](Self::post_task).
101+
fn post_non_nestable_task(&self, isolate_ptr: *mut std::ffi::c_void) {}
102+
103+
/// Called when `TaskRunner::PostDelayedTask` is invoked.
104+
///
105+
/// The task has been forwarded to the default runner's delayed queue.
106+
/// `delay_in_seconds` is the delay before the task should execute.
107+
/// Embedders should schedule a wake-up after this delay.
108+
///
109+
/// May be called from ANY thread.
110+
fn post_delayed_task(
111+
&self,
112+
isolate_ptr: *mut std::ffi::c_void,
113+
delay_in_seconds: f64,
114+
) {
115+
}
116+
117+
/// Called when `TaskRunner::PostNonNestableDelayedTask` is invoked.
118+
///
119+
/// Same semantics as [`post_delayed_task`](Self::post_delayed_task).
120+
fn post_non_nestable_delayed_task(
121+
&self,
122+
isolate_ptr: *mut std::ffi::c_void,
123+
delay_in_seconds: f64,
124+
) {
125+
}
126+
127+
/// Called when `TaskRunner::PostIdleTask` is invoked.
128+
///
129+
/// Same semantics as [`post_task`](Self::post_task).
130+
fn post_idle_task(&self, isolate_ptr: *mut std::ffi::c_void) {}
131+
}
132+
133+
// FFI callbacks called from C++ CustomPlatform/CustomTaskRunner.
134+
// `context` is a raw pointer to a `Box<dyn PlatformImpl>`.
135+
136+
#[unsafe(no_mangle)]
137+
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostTask(
138+
context: *mut std::ffi::c_void,
139+
isolate: *mut std::ffi::c_void,
140+
) {
141+
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
142+
imp.post_task(isolate);
143+
}
144+
145+
#[unsafe(no_mangle)]
146+
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostNonNestableTask(
147+
context: *mut std::ffi::c_void,
148+
isolate: *mut std::ffi::c_void,
149+
) {
150+
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
151+
imp.post_non_nestable_task(isolate);
152+
}
153+
154+
#[unsafe(no_mangle)]
155+
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostDelayedTask(
156+
context: *mut std::ffi::c_void,
157+
isolate: *mut std::ffi::c_void,
158+
delay_in_seconds: f64,
159+
) {
160+
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
161+
imp.post_delayed_task(isolate, delay_in_seconds);
162+
}
163+
164+
#[unsafe(no_mangle)]
165+
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostNonNestableDelayedTask(
166+
context: *mut std::ffi::c_void,
167+
isolate: *mut std::ffi::c_void,
168+
delay_in_seconds: f64,
169+
) {
170+
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
171+
imp.post_non_nestable_delayed_task(isolate, delay_in_seconds);
172+
}
173+
174+
#[unsafe(no_mangle)]
175+
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostIdleTask(
176+
context: *mut std::ffi::c_void,
177+
isolate: *mut std::ffi::c_void,
178+
) {
179+
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
180+
imp.post_idle_task(isolate);
181+
}
182+
183+
#[unsafe(no_mangle)]
184+
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__DROP(
185+
context: *mut std::ffi::c_void,
186+
) {
187+
unsafe {
188+
let _ = Box::from_raw(context as *mut Box<dyn PlatformImpl>);
189+
}
190+
}
191+
63192
/// Returns a new instance of the default v8::Platform implementation.
64193
///
65194
/// |thread_pool_size| is the number of worker threads to allocate for
@@ -111,6 +240,31 @@ pub fn new_single_threaded_default_platform(
111240
Platform::new_single_threaded(idle_task_support)
112241
}
113242

243+
/// Creates a custom platform backed by `DefaultPlatform` that delegates
244+
/// virtual method overrides to the provided [`PlatformImpl`] trait object.
245+
///
246+
/// This follows the same pattern as
247+
/// [`V8InspectorClient::new`](crate::inspector::V8InspectorClient::new).
248+
///
249+
/// When `unprotected` is true, thread-isolated allocations are disabled
250+
/// (same as `new_unprotected_default_platform`). This is required when
251+
/// isolates may be created on threads other than the one that called
252+
/// `V8::initialize`.
253+
#[inline(always)]
254+
pub fn new_custom_platform(
255+
thread_pool_size: u32,
256+
idle_task_support: bool,
257+
unprotected: bool,
258+
platform_impl: impl PlatformImpl + 'static,
259+
) -> UniqueRef<Platform> {
260+
Platform::new_custom(
261+
thread_pool_size,
262+
idle_task_support,
263+
unprotected,
264+
platform_impl,
265+
)
266+
}
267+
114268
impl Platform {
115269
/// Returns a new instance of the default v8::Platform implementation.
116270
///
@@ -175,6 +329,34 @@ impl Platform {
175329
))
176330
}
177331
}
332+
333+
/// Creates a custom platform backed by `DefaultPlatform` that delegates
334+
/// virtual method overrides to the provided [`PlatformImpl`] trait object.
335+
///
336+
/// The trait object is owned by the platform and will be dropped when the
337+
/// platform is destroyed.
338+
#[inline(always)]
339+
pub fn new_custom(
340+
thread_pool_size: u32,
341+
idle_task_support: bool,
342+
unprotected: bool,
343+
platform_impl: impl PlatformImpl + 'static,
344+
) -> UniqueRef<Self> {
345+
// Double-box: inner Box<dyn> is a fat pointer, outer Box gives us a
346+
// thin pointer we can pass through C++ void*.
347+
let boxed: Box<dyn PlatformImpl> = Box::new(platform_impl);
348+
let context = Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void;
349+
// thread_pool_size clamping (0 → hardware_concurrency, max 16) is
350+
// handled on the C++ side in v8__Platform__NewCustomPlatform.
351+
unsafe {
352+
UniqueRef::from_raw(v8__Platform__NewCustomPlatform(
353+
thread_pool_size as i32,
354+
idle_task_support,
355+
unprotected,
356+
context,
357+
))
358+
}
359+
}
178360
}
179361

180362
impl Platform {

0 commit comments

Comments
 (0)