Skip to content

Commit 87082f0

Browse files
bartlomiejuclaude
andauthored
feat: add bindings for v8::WasmModuleCompilation (#1908)
## Summary Add Rust bindings for the new experimental `v8::WasmModuleCompilation` class from `v8/include/v8-wasm.h`, providing an interface for asynchronous WebAssembly module compilation (e.g. for source phase imports). ### Bindings provided - `WasmModuleCompilation::new()` / `Default` — create a new compilation - `on_bytes_received(&mut self, data: &[u8])` — feed wasm bytes (callable from any thread) - `finish(self, scope, caching_callback, resolution_callback)` — finalize compilation on the main thread; the resolution callback receives `Result<Local<WasmModuleObject>, Local<Value>>` along with `&Isolate` for creating `Global` handles - `abort(self)` — cancel compilation - `set_has_compiled_module_bytes(&mut self)` — signal cached module bytes are available - `set_more_functions_can_be_serialized_callback(...)` — register a serialization callback - `set_url(&mut self, url: &str)` — set source URL before finishing - `Drop` cleans up the C++ allocation; `Send` is implemented (V8 allows cross-thread usage) ### Implementation details - Resolution callback lifetime is managed via `shared_ptr<void>` with a custom deleter on the C++ side, which correctly handles `std::function` copy semantics when V8 internally copies the callback into its `Resolver` - Rust side uses `Option<Box<dyn FnOnce>>` so the trampoline can `.take()` the closure (FnOnce semantics) without freeing the outer allocation, which is ref-counted by `shared_ptr` - Serialization callback uses the same `shared_ptr<void>` pattern - Isolate pointer is captured in the Rust closure rather than threaded through C++ - Caching callback function pointer is passed directly to V8 without wrapping --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ea5769e commit 87082f0

File tree

4 files changed

+377
-2
lines changed

4 files changed

+377
-2
lines changed

src/binding.cc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3791,6 +3791,80 @@ uint32_t v8__ValueDeserializer__GetWireFormatVersion(
37913791
}
37923792
} // extern "C"
37933793

3794+
// v8::WasmModuleCompilation
3795+
3796+
extern "C" {
3797+
3798+
v8::WasmModuleCompilation* v8__WasmModuleCompilation__NEW() {
3799+
return new v8::WasmModuleCompilation();
3800+
}
3801+
3802+
void v8__WasmModuleCompilation__DELETE(v8::WasmModuleCompilation* self) {
3803+
delete self;
3804+
}
3805+
3806+
void v8__WasmModuleCompilation__OnBytesReceived(v8::WasmModuleCompilation* self,
3807+
const uint8_t* bytes,
3808+
size_t size) {
3809+
self->OnBytesReceived(bytes, size);
3810+
}
3811+
3812+
void v8__WasmModuleCompilation__Finish(
3813+
v8::WasmModuleCompilation* self, v8::Isolate* isolate,
3814+
void (*caching_callback)(v8::WasmStreaming::ModuleCachingInterface&),
3815+
void (*resolution_callback)(void* data, const v8::WasmModuleObject* module,
3816+
const v8::Value* error),
3817+
void* resolution_data, void (*drop_resolution_data)(void* data)) {
3818+
// Use shared_ptr to reference-count the Rust closure data through
3819+
// std::function's copy semantics. The custom deleter calls back into Rust
3820+
// to drop the boxed closure when the last copy is destroyed.
3821+
auto shared_data = std::shared_ptr<void>(
3822+
resolution_data,
3823+
[drop_resolution_data](void* p) { drop_resolution_data(p); });
3824+
self->Finish(
3825+
isolate, caching_callback,
3826+
[resolution_callback, shared_data](auto result) {
3827+
if (auto* module =
3828+
std::get_if<v8::Local<v8::WasmModuleObject>>(&result)) {
3829+
resolution_callback(shared_data.get(), local_to_ptr(*module),
3830+
nullptr);
3831+
} else {
3832+
resolution_callback(
3833+
shared_data.get(), nullptr,
3834+
local_to_ptr(std::get<v8::Local<v8::Value>>(result)));
3835+
}
3836+
});
3837+
}
3838+
3839+
void v8__WasmModuleCompilation__Abort(v8::WasmModuleCompilation* self) {
3840+
self->Abort();
3841+
}
3842+
3843+
void v8__WasmModuleCompilation__SetHasCompiledModuleBytes(
3844+
v8::WasmModuleCompilation* self) {
3845+
self->SetHasCompiledModuleBytes();
3846+
}
3847+
3848+
void v8__WasmModuleCompilation__SetMoreFunctionsCanBeSerializedCallback(
3849+
v8::WasmModuleCompilation* self,
3850+
void (*callback)(void* data, v8::CompiledWasmModule* compiled_module),
3851+
void* data, void (*drop_data)(void* data)) {
3852+
auto shared_data =
3853+
std::shared_ptr<void>(data, [drop_data](void* p) { drop_data(p); });
3854+
self->SetMoreFunctionsCanBeSerializedCallback(
3855+
[callback, shared_data](v8::CompiledWasmModule module) {
3856+
auto* heap_module = new v8::CompiledWasmModule(std::move(module));
3857+
callback(shared_data.get(), heap_module);
3858+
});
3859+
}
3860+
3861+
void v8__WasmModuleCompilation__SetUrl(v8::WasmModuleCompilation* self,
3862+
const char* url, size_t length) {
3863+
self->SetUrl(url, length);
3864+
}
3865+
3866+
} // extern "C"
3867+
37943868
// v8::CompiledWasmModule
37953869

37963870
extern "C" {

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ pub use value_serializer::ValueSerializerHelper;
185185
pub use value_serializer::ValueSerializerImpl;
186186
pub use wasm::CompiledWasmModule;
187187
pub use wasm::ModuleCachingInterface;
188+
pub use wasm::WasmModuleCompilation;
188189
pub use wasm::WasmStreaming;
189190

190191
/// https://v8.dev/docs/version-numbers

src/wasm.rs

Lines changed: 248 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// Copyright 2019-2021 the Deno authors. All rights reserved. MIT license.
22

3+
use std::ffi::c_void;
4+
use std::ptr::null;
5+
use std::ptr::null_mut;
6+
37
use crate::ArrayBuffer;
8+
use crate::Isolate;
49
use crate::Local;
510
use crate::PinScope;
611
use crate::Value;
@@ -18,8 +23,6 @@ use crate::support::Opaque;
1823
use crate::support::ToCFn;
1924
use crate::support::UnitType;
2025
use crate::support::char;
21-
use std::ptr::null;
22-
use std::ptr::null_mut;
2326

2427
// Type-erased std::shared_ptr<v8::WasmStreaming>. Assumes it's safe
2528
// to move around (no backlinks). Not generally true for shared_ptrs
@@ -275,6 +278,209 @@ impl Drop for CompiledWasmModule {
275278
}
276279
}
277280

281+
// Type-erased v8::WasmModuleCompilation allocated on the C++ heap.
282+
#[repr(C)]
283+
struct InternalWasmModuleCompilation(Opaque);
284+
285+
/// An interface for asynchronous WebAssembly module compilation, to be used
286+
/// e.g. for implementing source phase imports.
287+
///
288+
/// Note: This interface is experimental and can change or be removed without
289+
/// notice.
290+
pub struct WasmModuleCompilation(*mut InternalWasmModuleCompilation);
291+
292+
// OnBytesReceived can be called from any thread per V8 documentation.
293+
unsafe impl Send for WasmModuleCompilation {}
294+
295+
impl WasmModuleCompilation {
296+
/// Start an asynchronous module compilation. This can be called on any
297+
/// thread.
298+
#[inline(always)]
299+
pub fn new() -> Self {
300+
unsafe { WasmModuleCompilation(v8__WasmModuleCompilation__NEW()) }
301+
}
302+
303+
/// Pass a new chunk of bytes to WebAssembly compilation. The buffer is
304+
/// owned by the caller and will not be accessed after this call returns.
305+
/// Can be called from any thread.
306+
#[inline(always)]
307+
pub fn on_bytes_received(&mut self, data: &[u8]) {
308+
unsafe {
309+
v8__WasmModuleCompilation__OnBytesReceived(
310+
self.0,
311+
data.as_ptr(),
312+
data.len(),
313+
);
314+
}
315+
}
316+
317+
/// Finish compilation. Must be called on the main thread after all bytes
318+
/// were passed to [`Self::on_bytes_received`].
319+
///
320+
/// The `resolution_callback` will eventually be called with either the
321+
/// compiled module or a compilation error. The callback receives `&Isolate`
322+
/// so that [`crate::Global`] handles can be created from the [`Local`]
323+
/// handles to persist them beyond the callback.
324+
///
325+
/// Must not be called after [`Self::abort`].
326+
#[inline(always)]
327+
pub fn finish(
328+
self,
329+
scope: &mut PinScope,
330+
caching_callback: Option<ModuleCachingCallback>,
331+
resolution_callback: impl FnOnce(
332+
&Isolate,
333+
Result<Local<'_, WasmModuleObject>, Local<'_, Value>>,
334+
) + 'static,
335+
) {
336+
// Capture the isolate pointer in the closure so it doesn't need to be
337+
// threaded through C++.
338+
let isolate_ptr = scope.get_isolate_ptr();
339+
let wrapped = move |module: *const WasmModuleObject,
340+
error: *const Value| {
341+
let isolate = unsafe { Isolate::from_raw_ptr(isolate_ptr) };
342+
if !module.is_null() {
343+
resolution_callback(
344+
&isolate,
345+
Ok(unsafe { Local::from_raw(module) }.unwrap()),
346+
);
347+
} else {
348+
resolution_callback(
349+
&isolate,
350+
Err(unsafe { Local::from_raw(error) }.unwrap()),
351+
);
352+
}
353+
};
354+
355+
// Double-box with Option: the outer Box gives us a thin pointer suitable
356+
// for void*. The Option allows the trampoline to .take() the closure
357+
// (FnOnce semantics) without freeing the outer allocation, which is
358+
// ref-counted by shared_ptr on the C++ side.
359+
#[allow(clippy::type_complexity)]
360+
let boxed: Box<
361+
Option<Box<dyn FnOnce(*const WasmModuleObject, *const Value)>>,
362+
> = Box::new(Some(Box::new(wrapped)));
363+
let data = Box::into_raw(boxed) as *mut c_void;
364+
365+
unsafe {
366+
v8__WasmModuleCompilation__Finish(
367+
self.0,
368+
scope.get_isolate_ptr(),
369+
caching_callback,
370+
resolution_trampoline,
371+
data,
372+
drop_resolution_data,
373+
);
374+
}
375+
}
376+
377+
/// Abort compilation. Can be called from any thread.
378+
/// Must not be called repeatedly, or after [`Self::finish`].
379+
#[inline(always)]
380+
pub fn abort(self) {
381+
unsafe { v8__WasmModuleCompilation__Abort(self.0) }
382+
}
383+
384+
/// Mark that the embedder has (potentially) cached compiled module bytes
385+
/// (i.e. a serialized [`CompiledWasmModule`]) that could match this
386+
/// compilation request. This will cause V8 to skip streaming compilation.
387+
/// The embedder should then pass a caching callback to [`Self::finish`].
388+
#[inline(always)]
389+
pub fn set_has_compiled_module_bytes(&mut self) {
390+
unsafe {
391+
v8__WasmModuleCompilation__SetHasCompiledModuleBytes(self.0);
392+
}
393+
}
394+
395+
/// Sets a callback which is called whenever a significant number of new
396+
/// functions are ready for serialization.
397+
#[inline(always)]
398+
pub fn set_more_functions_can_be_serialized_callback(
399+
&mut self,
400+
callback: impl Fn(CompiledWasmModule) + Send + 'static,
401+
) {
402+
let boxed: Box<Box<dyn Fn(CompiledWasmModule) + Send>> =
403+
Box::new(Box::new(callback));
404+
let data = Box::into_raw(boxed) as *mut c_void;
405+
406+
unsafe {
407+
v8__WasmModuleCompilation__SetMoreFunctionsCanBeSerializedCallback(
408+
self.0,
409+
serialization_trampoline,
410+
data,
411+
drop_serialization_data,
412+
);
413+
}
414+
}
415+
416+
/// Sets the UTF-8 encoded source URL for the `Script` object. This must
417+
/// be called before [`Self::finish`].
418+
#[inline(always)]
419+
pub fn set_url(&mut self, url: &str) {
420+
// V8 requires the url to be null terminated.
421+
let null_terminated_url = format!("{url}\0");
422+
unsafe {
423+
v8__WasmModuleCompilation__SetUrl(
424+
self.0,
425+
null_terminated_url.as_ptr() as *const char,
426+
url.len(),
427+
);
428+
}
429+
}
430+
}
431+
432+
impl Default for WasmModuleCompilation {
433+
fn default() -> Self {
434+
Self::new()
435+
}
436+
}
437+
438+
impl Drop for WasmModuleCompilation {
439+
fn drop(&mut self) {
440+
unsafe { v8__WasmModuleCompilation__DELETE(self.0) }
441+
}
442+
}
443+
444+
unsafe extern "C" fn resolution_trampoline(
445+
data: *mut c_void,
446+
module: *const WasmModuleObject,
447+
error: *const Value,
448+
) {
449+
// Take the closure out of the Option without freeing the outer Box.
450+
// The outer Box is ref-counted by shared_ptr on the C++ side and will
451+
// be freed via drop_resolution_data when the last copy is destroyed.
452+
let slot = unsafe {
453+
&mut *(data
454+
as *mut Option<Box<dyn FnOnce(*const WasmModuleObject, *const Value)>>)
455+
};
456+
let callback = slot.take().unwrap();
457+
callback(module, error);
458+
}
459+
460+
unsafe extern "C" fn drop_resolution_data(data: *mut c_void) {
461+
let _ = unsafe {
462+
Box::from_raw(
463+
data
464+
as *mut Option<Box<dyn FnOnce(*const WasmModuleObject, *const Value)>>,
465+
)
466+
};
467+
}
468+
469+
unsafe extern "C" fn serialization_trampoline(
470+
data: *mut c_void,
471+
compiled_module: *mut InternalCompiledWasmModule,
472+
) {
473+
let callback =
474+
unsafe { &**(data as *const Box<dyn Fn(CompiledWasmModule) + Send>) };
475+
callback(CompiledWasmModule(compiled_module));
476+
}
477+
478+
unsafe extern "C" fn drop_serialization_data(data: *mut c_void) {
479+
let _ = unsafe {
480+
Box::from_raw(data as *mut Box<dyn Fn(CompiledWasmModule) + Send>)
481+
};
482+
}
483+
278484
impl WasmMemoryObject {
279485
/// Returns underlying ArrayBuffer.
280486
#[inline(always)]
@@ -380,4 +586,44 @@ unsafe extern "C" {
380586
fn v8__WasmMemoryObject__Buffer(
381587
this: *const WasmMemoryObject,
382588
) -> *mut ArrayBuffer;
589+
590+
fn v8__WasmModuleCompilation__NEW() -> *mut InternalWasmModuleCompilation;
591+
fn v8__WasmModuleCompilation__DELETE(
592+
this: *mut InternalWasmModuleCompilation,
593+
);
594+
fn v8__WasmModuleCompilation__OnBytesReceived(
595+
this: *mut InternalWasmModuleCompilation,
596+
bytes: *const u8,
597+
size: usize,
598+
);
599+
fn v8__WasmModuleCompilation__Finish(
600+
this: *mut InternalWasmModuleCompilation,
601+
isolate: *mut RealIsolate,
602+
caching_callback: Option<ModuleCachingCallback>,
603+
resolution_callback: unsafe extern "C" fn(
604+
*mut c_void,
605+
*const WasmModuleObject,
606+
*const Value,
607+
),
608+
resolution_data: *mut c_void,
609+
drop_resolution_data: unsafe extern "C" fn(*mut c_void),
610+
);
611+
fn v8__WasmModuleCompilation__Abort(this: *mut InternalWasmModuleCompilation);
612+
fn v8__WasmModuleCompilation__SetHasCompiledModuleBytes(
613+
this: *mut InternalWasmModuleCompilation,
614+
);
615+
fn v8__WasmModuleCompilation__SetMoreFunctionsCanBeSerializedCallback(
616+
this: *mut InternalWasmModuleCompilation,
617+
callback: unsafe extern "C" fn(
618+
*mut c_void,
619+
*mut InternalCompiledWasmModule,
620+
),
621+
data: *mut c_void,
622+
drop_data: unsafe extern "C" fn(*mut c_void),
623+
);
624+
fn v8__WasmModuleCompilation__SetUrl(
625+
this: *mut InternalWasmModuleCompilation,
626+
url: *const char,
627+
length: usize,
628+
);
383629
}

0 commit comments

Comments
 (0)