Skip to content
141 changes: 141 additions & 0 deletions src/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <map>
#include <mutex>
#include <thread>

#include "cppgc/allocation.h"
Expand Down Expand Up @@ -3010,6 +3012,130 @@ v8::StartupData v8__SnapshotCreator__CreateBlob(
return self->CreateBlob(function_code_handling);
}

// Rust-side callbacks for the trait-based NotifyingPlatform.
// `context` is a pointer to a Rust Box<dyn ForegroundTaskCallback>.
extern "C" {
void v8__Platform__NotifyingPlatform__onForegroundTaskPosted(
void* context, void* isolate, double delay_in_seconds);
void v8__Platform__NotifyingPlatform__dropContext(void* context);
}

// TaskRunner wrapper that intercepts all PostTask* calls and notifies
// the embedder (via a Rust trait) before delegating to the real runner.
class NotifyingTaskRunner final : public v8::TaskRunner {
public:
NotifyingTaskRunner(std::shared_ptr<v8::TaskRunner> wrapped, void* context,
v8::Isolate* isolate)
: wrapped_(std::move(wrapped)), context_(context), isolate_(isolate) {}

bool IdleTasksEnabled() override { return wrapped_->IdleTasksEnabled(); }
bool NonNestableTasksEnabled() const override {
return wrapped_->NonNestableTasksEnabled();
}
bool NonNestableDelayedTasksEnabled() const override {
return wrapped_->NonNestableDelayedTasksEnabled();
}

protected:
void PostTaskImpl(std::unique_ptr<v8::Task> task,
const v8::SourceLocation& location) override {
wrapped_->PostTask(std::move(task), location);
v8__Platform__NotifyingPlatform__onForegroundTaskPosted(
context_, static_cast<void*>(isolate_), 0.0);
}
void PostNonNestableTaskImpl(std::unique_ptr<v8::Task> task,
const v8::SourceLocation& location) override {
wrapped_->PostNonNestableTask(std::move(task), location);
v8__Platform__NotifyingPlatform__onForegroundTaskPosted(
context_, static_cast<void*>(isolate_), 0.0);
}
void PostDelayedTaskImpl(std::unique_ptr<v8::Task> task,
double delay_in_seconds,
const v8::SourceLocation& location) override {
wrapped_->PostDelayedTask(std::move(task), delay_in_seconds, location);
v8__Platform__NotifyingPlatform__onForegroundTaskPosted(
context_, static_cast<void*>(isolate_),
delay_in_seconds > 0 ? delay_in_seconds : 0.0);
}
void PostNonNestableDelayedTaskImpl(
std::unique_ptr<v8::Task> task, double delay_in_seconds,
const v8::SourceLocation& location) override {
wrapped_->PostNonNestableDelayedTask(std::move(task), delay_in_seconds,
location);
v8__Platform__NotifyingPlatform__onForegroundTaskPosted(
context_, static_cast<void*>(isolate_),
delay_in_seconds > 0 ? delay_in_seconds : 0.0);
}
void PostIdleTaskImpl(std::unique_ptr<v8::IdleTask> task,
const v8::SourceLocation& location) override {
wrapped_->PostIdleTask(std::move(task), location);
v8__Platform__NotifyingPlatform__onForegroundTaskPosted(
context_, static_cast<void*>(isolate_), 0.0);
}

private:
std::shared_ptr<v8::TaskRunner> wrapped_;
void* context_;
v8::Isolate* isolate_;
};

// Platform wrapper that intercepts GetForegroundTaskRunner to return
// NotifyingTaskRunner instances, dispatching to a Rust trait object.
class NotifyingPlatform : public v8::platform::DefaultPlatform {
using IdleTaskSupport = v8::platform::IdleTaskSupport;

public:
NotifyingPlatform(int thread_pool_size, IdleTaskSupport idle_task_support,
void* context)
: DefaultPlatform(thread_pool_size, idle_task_support),
context_(context) {}

~NotifyingPlatform() override {
v8__Platform__NotifyingPlatform__dropContext(context_);
}

std::shared_ptr<v8::TaskRunner> GetForegroundTaskRunner(
v8::Isolate* isolate, v8::TaskPriority priority) override {
auto original = DefaultPlatform::GetForegroundTaskRunner(isolate, priority);
std::lock_guard<std::mutex> lock(mutex_);
auto key = std::make_pair(isolate, priority);
auto it = runners_.find(key);
if (it != runners_.end()) {
auto runner = it->second.lock();
if (runner) return runner;
}
auto notifying =
std::make_shared<NotifyingTaskRunner>(original, context_, isolate);
runners_[key] = notifying;
return notifying;
}

void NotifyIsolateShutdown(v8::Isolate* isolate) {
{
std::lock_guard<std::mutex> lock(mutex_);
for (auto it = runners_.begin(); it != runners_.end();) {
if (it->first.first == isolate) {
it = runners_.erase(it);
} else {
++it;
}
}
}
DefaultPlatform::NotifyIsolateShutdown(isolate);
}

v8::ThreadIsolatedAllocator* GetThreadIsolatedAllocator() override {
return nullptr;
}

private:
void* context_;
std::mutex mutex_;
std::map<std::pair<v8::Isolate*, v8::TaskPriority>,
std::weak_ptr<NotifyingTaskRunner>>
runners_;
};

class UnprotectedDefaultPlatform : public v8::platform::DefaultPlatform {
using IdleTaskSupport = v8::platform::IdleTaskSupport;
using InProcessStackDumping = v8::platform::InProcessStackDumping;
Expand Down Expand Up @@ -3063,6 +3189,21 @@ v8::Platform* v8__Platform__NewSingleThreadedDefaultPlatform(
.release();
}

v8::Platform* v8__Platform__NewNotifyingPlatform(int thread_pool_size,
bool idle_task_support,
void* context) {
if (thread_pool_size < 1) {
thread_pool_size = std::thread::hardware_concurrency();
}
thread_pool_size = std::max(std::min(thread_pool_size, 16), 1);
return std::make_unique<NotifyingPlatform>(
thread_pool_size,
idle_task_support ? v8::platform::IdleTaskSupport::kEnabled
: v8::platform::IdleTaskSupport::kDisabled,
context)
.release();
}

bool v8__Platform__PumpMessageLoop(v8::Platform* platform, v8::Isolate* isolate,
bool wait_for_work) {
return v8::platform::PumpMessageLoop(
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,10 @@ pub use isolate_create_params::CreateParams;
pub use microtask::MicrotaskQueue;
pub use module::*;
pub use object::*;
pub use platform::ForegroundTaskCallback;
pub use platform::Platform;
pub use platform::new_default_platform;
pub use platform::new_notifying_platform;
pub use platform::new_single_threaded_default_platform;
pub use platform::new_unprotected_default_platform;
pub use primitives::*;
Expand Down
97 changes: 97 additions & 0 deletions src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ unsafe extern "C" {
fn v8__Platform__NewSingleThreadedDefaultPlatform(
idle_task_support: bool,
) -> *mut Platform;
fn v8__Platform__NewNotifyingPlatform(
thread_pool_size: int,
idle_task_support: bool,
context: *mut std::ffi::c_void,
) -> *mut Platform;
fn v8__Platform__DELETE(this: *mut Platform);

fn v8__Platform__PumpMessageLoop(
Expand Down Expand Up @@ -60,6 +65,51 @@ unsafe extern "C" {
#[derive(Debug)]
pub struct Platform(Opaque);

/// Trait for receiving notifications when foreground tasks are posted to an
/// isolate's task runner. Implementations must be thread-safe as callbacks
/// can fire from any V8 background thread.
///
/// This follows the trait-based pattern used by the inspector API
/// (`V8InspectorClientImpl`, `ChannelImpl`).
pub trait ForegroundTaskCallback: Send + Sync {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by trait i meant being able to build your own platform by providing a trait that represents the various virtual c++ methods. so it wouldn't be specifically a "notifying platform". again you can refer to the inspector code to see what this sort of pattern looks like.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense! I created a PlatformImpl, wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the trait methods should just be the the virtual c++ methods that are being overridden. there should not be any onForegroundTaskPosted or anything extra like that.

/// Called when a foreground task has been posted for the given isolate.
///
/// `isolate_ptr` is the raw `v8::Isolate*` pointer of the target isolate.
/// `delay_in_seconds` is 0.0 for immediate tasks, or the delay before the
/// task should be executed. For delayed tasks, the embedder should schedule
/// a wake-up after the given delay (e.g. via a timer in tokio).
///
/// This may be called from ANY thread (V8 background threads, etc.).
fn on_foreground_task_posted(
&self,
isolate_ptr: *mut std::ffi::c_void,
delay_in_seconds: f64,
);
}

// FFI callbacks called from C++ NotifyingPlatform/NotifyingTaskRunner.
// `context` is a raw pointer to a `Box<dyn ForegroundTaskCallback>`.

#[unsafe(no_mangle)]
unsafe extern "C" fn v8__Platform__NotifyingPlatform__onForegroundTaskPosted(
context: *mut std::ffi::c_void,
isolate: *mut std::ffi::c_void,
delay_in_seconds: f64,
) {
let callback =
unsafe { &*(context as *const Box<dyn ForegroundTaskCallback>) };
callback.on_foreground_task_posted(isolate, delay_in_seconds);
}

#[unsafe(no_mangle)]
unsafe extern "C" fn v8__Platform__NotifyingPlatform__dropContext(
context: *mut std::ffi::c_void,
) {
unsafe {
let _ = Box::from_raw(context as *mut Box<dyn ForegroundTaskCallback>);
}
}

/// Returns a new instance of the default v8::Platform implementation.
///
/// |thread_pool_size| is the number of worker threads to allocate for
Expand Down Expand Up @@ -111,6 +161,28 @@ pub fn new_single_threaded_default_platform(
Platform::new_single_threaded(idle_task_support)
}

/// Creates a NotifyingPlatform that wraps DefaultPlatform and calls the
/// provided [`ForegroundTaskCallback`] whenever a foreground task is posted
/// for any isolate.
///
/// This allows embedders to wake their event loop when V8 background threads
/// complete work and post foreground continuations (e.g. background compilation
/// finishing, Atomics.waitAsync resolving). For delayed tasks, the embedder
/// should schedule a wake-up after `delay_in_seconds` (e.g. via a timer).
///
/// The callback may be invoked from ANY thread (V8 background threads, etc.)
/// and must be safe to call concurrently.
///
/// Thread-isolated allocations are disabled (same as `new_unprotected_default_platform`).
#[inline(always)]
pub fn new_notifying_platform(
thread_pool_size: u32,
idle_task_support: bool,
callback: impl ForegroundTaskCallback + 'static,
) -> UniqueRef<Platform> {
Platform::new_notifying(thread_pool_size, idle_task_support, callback)
}

impl Platform {
/// Returns a new instance of the default v8::Platform implementation.
///
Expand Down Expand Up @@ -175,6 +247,31 @@ impl Platform {
))
}
}

/// Creates a NotifyingPlatform (subclass of DefaultPlatform) that dispatches
/// to the provided [`ForegroundTaskCallback`] whenever a foreground task is
/// posted for an isolate.
///
/// The callback trait object is owned by the platform and will be dropped
/// when the platform is destroyed.
#[inline(always)]
pub fn new_notifying(
thread_pool_size: u32,
idle_task_support: bool,
callback: impl ForegroundTaskCallback + 'static,
) -> UniqueRef<Self> {
// Double-box: inner Box<dyn> is a fat pointer, outer Box gives us a
// thin pointer we can pass through C++ void*.
let boxed: Box<dyn ForegroundTaskCallback> = Box::new(callback);
let context = Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void;
unsafe {
UniqueRef::from_raw(v8__Platform__NewNotifyingPlatform(
thread_pool_size.min(16) as i32,
idle_task_support,
context,
))
}
}
}

impl Platform {
Expand Down