Skip to content

Commit be7d65e

Browse files
bartlomiejuclaude
andauthored
feat: transfer foreground task ownership to Rust in PlatformImpl (#1934)
Instead of forwarding tasks to DefaultPlatform's queue and notifying Rust, CustomTaskRunner now transfers task ownership to Rust via the PlatformImpl trait. This allows embedders (e.g. deno_core) to schedule tasks on their own event loop (e.g. tokio::spawn) and call task.run() directly, rather than relying on PumpMessageLoop. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0876010 commit be7d65e

4 files changed

Lines changed: 199 additions & 66 deletions

File tree

src/binding.cc

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3015,22 +3015,40 @@ v8::StartupData v8__SnapshotCreator__CreateBlob(
30153015
// Rust-side callbacks for trait-based CustomPlatform (PlatformImpl trait).
30163016
// Each callback corresponds to a C++ virtual method on TaskRunner or Platform.
30173017
// `context` is a pointer to the Rust Box<dyn PlatformImpl>.
3018+
// Task ownership is transferred to Rust — Rust is responsible for calling
3019+
// Run() and deleting the task.
30183020
extern "C" {
3019-
void v8__Platform__CustomPlatform__BASE__PostTask(void* context, void* isolate);
3021+
void v8__Platform__CustomPlatform__BASE__PostTask(void* context, void* isolate,
3022+
v8::Task* task);
30203023
void v8__Platform__CustomPlatform__BASE__PostNonNestableTask(void* context,
3021-
void* isolate);
3024+
void* isolate,
3025+
v8::Task* task);
30223026
void v8__Platform__CustomPlatform__BASE__PostDelayedTask(
3023-
void* context, void* isolate, double delay_in_seconds);
3027+
void* context, void* isolate, v8::Task* task, double delay_in_seconds);
30243028
void v8__Platform__CustomPlatform__BASE__PostNonNestableDelayedTask(
3025-
void* context, void* isolate, double delay_in_seconds);
3029+
void* context, void* isolate, v8::Task* task, double delay_in_seconds);
30263030
void v8__Platform__CustomPlatform__BASE__PostIdleTask(void* context,
3027-
void* isolate);
3031+
void* isolate,
3032+
v8::IdleTask* task);
30283033
void v8__Platform__CustomPlatform__BASE__DROP(void* context);
30293034
}
30303035

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.
3036+
// FFI functions for running and deleting V8 tasks from Rust.
3037+
extern "C" {
3038+
void v8__Task__Run(v8::Task* task) { task->Run(); }
3039+
void v8__Task__DELETE(v8::Task* task) { delete task; }
3040+
void v8__IdleTask__Run(v8::IdleTask* task, double deadline_in_seconds) {
3041+
task->Run(deadline_in_seconds);
3042+
}
3043+
void v8__IdleTask__DELETE(v8::IdleTask* task) { delete task; }
3044+
}
3045+
3046+
// TaskRunner wrapper that intercepts all PostTask* virtual methods and
3047+
// transfers task ownership to Rust via the PlatformImpl trait. The Rust
3048+
// side is responsible for scheduling and calling task->Run().
3049+
//
3050+
// The wrapped runner is kept for capability queries (IdleTasksEnabled, etc.)
3051+
// but tasks are NOT forwarded to it — Rust owns them entirely.
30343052
class CustomTaskRunner final : public v8::TaskRunner {
30353053
public:
30363054
CustomTaskRunner(std::shared_ptr<v8::TaskRunner> wrapped, void* context,
@@ -3048,38 +3066,32 @@ class CustomTaskRunner final : public v8::TaskRunner {
30483066
protected:
30493067
void PostTaskImpl(std::unique_ptr<v8::Task> task,
30503068
const v8::SourceLocation& location) override {
3051-
wrapped_->PostTask(std::move(task), location);
3052-
v8__Platform__CustomPlatform__BASE__PostTask(context_,
3053-
static_cast<void*>(isolate_));
3069+
v8__Platform__CustomPlatform__BASE__PostTask(
3070+
context_, static_cast<void*>(isolate_), task.release());
30543071
}
30553072
void PostNonNestableTaskImpl(std::unique_ptr<v8::Task> task,
30563073
const v8::SourceLocation& location) override {
3057-
wrapped_->PostNonNestableTask(std::move(task), location);
30583074
v8__Platform__CustomPlatform__BASE__PostNonNestableTask(
3059-
context_, static_cast<void*>(isolate_));
3075+
context_, static_cast<void*>(isolate_), task.release());
30603076
}
30613077
void PostDelayedTaskImpl(std::unique_ptr<v8::Task> task,
30623078
double delay_in_seconds,
30633079
const v8::SourceLocation& location) override {
3064-
wrapped_->PostDelayedTask(std::move(task), delay_in_seconds, location);
30653080
v8__Platform__CustomPlatform__BASE__PostDelayedTask(
3066-
context_, static_cast<void*>(isolate_),
3081+
context_, static_cast<void*>(isolate_), task.release(),
30673082
delay_in_seconds > 0 ? delay_in_seconds : 0.0);
30683083
}
30693084
void PostNonNestableDelayedTaskImpl(
30703085
std::unique_ptr<v8::Task> task, double delay_in_seconds,
30713086
const v8::SourceLocation& location) override {
3072-
wrapped_->PostNonNestableDelayedTask(std::move(task), delay_in_seconds,
3073-
location);
30743087
v8__Platform__CustomPlatform__BASE__PostNonNestableDelayedTask(
3075-
context_, static_cast<void*>(isolate_),
3088+
context_, static_cast<void*>(isolate_), task.release(),
30763089
delay_in_seconds > 0 ? delay_in_seconds : 0.0);
30773090
}
30783091
void PostIdleTaskImpl(std::unique_ptr<v8::IdleTask> task,
30793092
const v8::SourceLocation& location) override {
3080-
wrapped_->PostIdleTask(std::move(task), location);
30813093
v8__Platform__CustomPlatform__BASE__PostIdleTask(
3082-
context_, static_cast<void*>(isolate_));
3094+
context_, static_cast<void*>(isolate_), task.release());
30833095
}
30843096

30853097
private:

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,10 @@ pub use isolate_create_params::CreateParams;
139139
pub use microtask::MicrotaskQueue;
140140
pub use module::*;
141141
pub use object::*;
142+
pub use platform::IdleTask;
142143
pub use platform::Platform;
143144
pub use platform::PlatformImpl;
145+
pub use platform::Task;
144146
pub use platform::new_custom_platform;
145147
pub use platform::new_default_platform;
146148
pub use platform::new_single_threaded_default_platform;

src/platform.rs

Lines changed: 143 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ unsafe extern "C" {
4747
isolate: *mut RealIsolate,
4848
);
4949

50+
fn v8__Task__Run(task: *mut std::ffi::c_void);
51+
fn v8__Task__DELETE(task: *mut std::ffi::c_void);
52+
fn v8__IdleTask__Run(task: *mut std::ffi::c_void, deadline_in_seconds: f64);
53+
fn v8__IdleTask__DELETE(task: *mut std::ffi::c_void);
54+
5055
fn std__shared_ptr__v8__Platform__CONVERT__std__unique_ptr(
5156
unique_ptr: UniquePtr<Platform>,
5257
) -> SharedPtrBase<Platform>;
@@ -66,52 +71,141 @@ unsafe extern "C" {
6671
#[derive(Debug)]
6772
pub struct Platform(Opaque);
6873

74+
/// A V8 foreground task. Ownership is transferred from C++ to Rust when
75+
/// V8 posts a task via [`PlatformImpl`] trait methods.
76+
///
77+
/// The embedder is responsible for scheduling the task and calling
78+
/// [`run()`](Task::run). For example, in an async runtime like tokio:
79+
///
80+
/// ```ignore
81+
/// tokio::spawn(async move { task.run() });
82+
/// ```
83+
///
84+
/// If dropped without calling `run()`, the task is destroyed without
85+
/// executing.
86+
pub struct Task(*mut std::ffi::c_void);
87+
88+
// SAFETY: V8 tasks are designed to be posted from background threads and
89+
// run on the isolate's foreground thread. The unique_ptr transfer is safe
90+
// across thread boundaries.
91+
unsafe impl Send for Task {}
92+
93+
impl Task {
94+
/// Run the task. Consumes self to prevent double execution.
95+
pub fn run(self) {
96+
let ptr = self.0;
97+
// Prevent Drop from deleting — we'll delete after Run.
98+
std::mem::forget(self);
99+
unsafe {
100+
v8__Task__Run(ptr);
101+
v8__Task__DELETE(ptr);
102+
}
103+
}
104+
}
105+
106+
impl Drop for Task {
107+
fn drop(&mut self) {
108+
unsafe { v8__Task__DELETE(self.0) };
109+
}
110+
}
111+
112+
/// A V8 idle task. Similar to [`Task`] but accepts a deadline parameter
113+
/// when run.
114+
///
115+
/// If dropped without calling `run()`, the task is destroyed without
116+
/// executing.
117+
pub struct IdleTask(*mut std::ffi::c_void);
118+
119+
// SAFETY: Same as Task — safe to transfer across threads.
120+
unsafe impl Send for IdleTask {}
121+
122+
impl IdleTask {
123+
/// Run the idle task with the given deadline. Consumes self.
124+
///
125+
/// `deadline_in_seconds` is the absolute time (in seconds since some
126+
/// epoch) by which the idle task should complete.
127+
pub fn run(self, deadline_in_seconds: f64) {
128+
let ptr = self.0;
129+
std::mem::forget(self);
130+
unsafe {
131+
v8__IdleTask__Run(ptr, deadline_in_seconds);
132+
v8__IdleTask__DELETE(ptr);
133+
}
134+
}
135+
}
136+
137+
impl Drop for IdleTask {
138+
fn drop(&mut self) {
139+
unsafe { v8__IdleTask__DELETE(self.0) };
140+
}
141+
}
142+
69143
/// Trait for customizing platform behavior, following the same pattern as
70144
/// [`V8InspectorClientImpl`](crate::inspector::V8InspectorClientImpl).
71145
///
72-
/// Implement this trait to receive callbacks for overridden C++ virtual
73-
/// methods on the `DefaultPlatform` and its per-isolate `TaskRunner`.
146+
/// Implement this trait to receive V8 foreground tasks and schedule them
147+
/// on your event loop. The C++ `CustomPlatform` wraps each isolate's
148+
/// `TaskRunner` so that every `PostTask` / `PostDelayedTask` / etc. call
149+
/// transfers task ownership to Rust through the corresponding trait method.
150+
///
151+
/// **The embedder is responsible for calling [`Task::run()`] on the
152+
/// isolate's thread.** For example, using tokio:
153+
///
154+
/// ```ignore
155+
/// fn post_task(&self, isolate_ptr: *mut c_void, task: Task) {
156+
/// tokio::spawn(async move { task.run() });
157+
/// }
74158
///
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.
159+
/// fn post_delayed_task(&self, isolate_ptr: *mut c_void, task: Task, delay: f64) {
160+
/// tokio::spawn(async move {
161+
/// tokio::time::sleep(Duration::from_secs_f64(delay)).await;
162+
/// task.run();
163+
/// });
164+
/// }
165+
/// ```
79166
///
80-
/// All methods have default no-op implementations; override only what
81-
/// you need.
167+
/// All methods have default implementations that run the task immediately
168+
/// (synchronously). Override to integrate with your event loop.
82169
///
83170
/// Implementations must be `Send + Sync` as callbacks may fire from any
84171
/// thread.
85172
#[allow(unused_variables)]
86173
pub trait PlatformImpl: Send + Sync {
87-
// ---- TaskRunner virtual methods ----
88-
89174
/// Called when `TaskRunner::PostTask` is invoked for the given isolate.
90175
///
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.
176+
/// The [`Task`] must be run on the isolate's foreground thread by calling
177+
/// [`Task::run()`].
94178
///
95179
/// May be called from ANY thread (V8 background threads, etc.).
96-
fn post_task(&self, isolate_ptr: *mut std::ffi::c_void) {}
180+
fn post_task(&self, isolate_ptr: *mut std::ffi::c_void, task: Task) {
181+
task.run();
182+
}
97183

98184
/// Called when `TaskRunner::PostNonNestableTask` is invoked.
99185
///
100-
/// Same semantics as [`post_task`](Self::post_task).
101-
fn post_non_nestable_task(&self, isolate_ptr: *mut std::ffi::c_void) {}
186+
/// Same semantics as [`post_task`](Self::post_task), but the task must
187+
/// not be run within a nested `PumpMessageLoop`.
188+
fn post_non_nestable_task(
189+
&self,
190+
isolate_ptr: *mut std::ffi::c_void,
191+
task: Task,
192+
) {
193+
task.run();
194+
}
102195

103196
/// Called when `TaskRunner::PostDelayedTask` is invoked.
104197
///
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.
198+
/// The task should be run after `delay_in_seconds` has elapsed.
199+
/// For example, using `tokio::time::sleep` or a timer wheel.
108200
///
109201
/// May be called from ANY thread.
110202
fn post_delayed_task(
111203
&self,
112204
isolate_ptr: *mut std::ffi::c_void,
205+
task: Task,
113206
delay_in_seconds: f64,
114207
) {
208+
task.run();
115209
}
116210

117211
/// Called when `TaskRunner::PostNonNestableDelayedTask` is invoked.
@@ -120,64 +214,75 @@ pub trait PlatformImpl: Send + Sync {
120214
fn post_non_nestable_delayed_task(
121215
&self,
122216
isolate_ptr: *mut std::ffi::c_void,
217+
task: Task,
123218
delay_in_seconds: f64,
124219
) {
220+
task.run();
125221
}
126222

127223
/// Called when `TaskRunner::PostIdleTask` is invoked.
128224
///
129-
/// Same semantics as [`post_task`](Self::post_task).
130-
fn post_idle_task(&self, isolate_ptr: *mut std::ffi::c_void) {}
225+
/// The [`IdleTask`] should be run when the embedder has idle time,
226+
/// passing the deadline via [`IdleTask::run(deadline)`](IdleTask::run).
227+
fn post_idle_task(&self, isolate_ptr: *mut std::ffi::c_void, task: IdleTask) {
228+
task.run(0.0);
229+
}
131230
}
132231

133232
// FFI callbacks called from C++ CustomPlatform/CustomTaskRunner.
134233
// `context` is a raw pointer to a `Box<dyn PlatformImpl>`.
234+
// Task pointers are owned — Rust is responsible for running and deleting them.
135235

136236
#[unsafe(no_mangle)]
137237
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostTask(
138238
context: *mut std::ffi::c_void,
139239
isolate: *mut std::ffi::c_void,
240+
task: *mut std::ffi::c_void,
140241
) {
141242
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
142-
imp.post_task(isolate);
243+
imp.post_task(isolate, Task(task));
143244
}
144245

145246
#[unsafe(no_mangle)]
146247
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostNonNestableTask(
147248
context: *mut std::ffi::c_void,
148249
isolate: *mut std::ffi::c_void,
250+
task: *mut std::ffi::c_void,
149251
) {
150252
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
151-
imp.post_non_nestable_task(isolate);
253+
imp.post_non_nestable_task(isolate, Task(task));
152254
}
153255

154256
#[unsafe(no_mangle)]
155257
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostDelayedTask(
156258
context: *mut std::ffi::c_void,
157259
isolate: *mut std::ffi::c_void,
260+
task: *mut std::ffi::c_void,
158261
delay_in_seconds: f64,
159262
) {
160263
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
161-
imp.post_delayed_task(isolate, delay_in_seconds);
264+
imp.post_delayed_task(isolate, Task(task), delay_in_seconds);
162265
}
163266

164267
#[unsafe(no_mangle)]
165268
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostNonNestableDelayedTask(
166269
context: *mut std::ffi::c_void,
167270
isolate: *mut std::ffi::c_void,
271+
task: *mut std::ffi::c_void,
168272
delay_in_seconds: f64,
169273
) {
170274
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
171-
imp.post_non_nestable_delayed_task(isolate, delay_in_seconds);
275+
imp.post_non_nestable_delayed_task(isolate, Task(task), delay_in_seconds);
172276
}
173277

174278
#[unsafe(no_mangle)]
175279
unsafe extern "C" fn v8__Platform__CustomPlatform__BASE__PostIdleTask(
176280
context: *mut std::ffi::c_void,
177281
isolate: *mut std::ffi::c_void,
282+
task: *mut std::ffi::c_void,
178283
) {
179284
let imp = unsafe { &*(context as *const Box<dyn PlatformImpl>) };
180-
imp.post_idle_task(isolate);
285+
imp.post_idle_task(isolate, IdleTask(task));
181286
}
182287

183288
#[unsafe(no_mangle)]
@@ -240,11 +345,15 @@ pub fn new_single_threaded_default_platform(
240345
Platform::new_single_threaded(idle_task_support)
241346
}
242347

243-
/// Creates a custom platform backed by `DefaultPlatform` that delegates
244-
/// virtual method overrides to the provided [`PlatformImpl`] trait object.
348+
/// Creates a custom platform backed by `DefaultPlatform` that transfers
349+
/// foreground task ownership to the provided [`PlatformImpl`] trait object.
245350
///
246-
/// This follows the same pattern as
247-
/// [`V8InspectorClient::new`](crate::inspector::V8InspectorClient::new).
351+
/// Unlike the default platform, foreground tasks are NOT queued internally.
352+
/// Instead, each `PostTask` / `PostDelayedTask` / etc. call transfers the
353+
/// [`Task`] to Rust via the trait. The embedder is responsible for
354+
/// scheduling and calling [`Task::run()`] on the isolate's thread.
355+
///
356+
/// Background tasks (thread pool) are still handled by `DefaultPlatform`.
248357
///
249358
/// When `unprotected` is true, thread-isolated allocations are disabled
250359
/// (same as `new_unprotected_default_platform`). This is required when
@@ -330,8 +439,10 @@ impl Platform {
330439
}
331440
}
332441

333-
/// Creates a custom platform backed by `DefaultPlatform` that delegates
334-
/// virtual method overrides to the provided [`PlatformImpl`] trait object.
442+
/// Creates a custom platform that transfers foreground task ownership to
443+
/// the provided [`PlatformImpl`] trait object.
444+
///
445+
/// See [`new_custom_platform`] for details.
335446
///
336447
/// The trait object is owned by the platform and will be dropped when the
337448
/// platform is destroyed.

0 commit comments

Comments
 (0)