diff --git a/src/binding.cc b/src/binding.cc index 50db9ec0d9..37d9df6b9a 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -3791,6 +3791,80 @@ uint32_t v8__ValueDeserializer__GetWireFormatVersion( } } // extern "C" +// v8::WasmModuleCompilation + +extern "C" { + +v8::WasmModuleCompilation* v8__WasmModuleCompilation__NEW() { + return new v8::WasmModuleCompilation(); +} + +void v8__WasmModuleCompilation__DELETE(v8::WasmModuleCompilation* self) { + delete self; +} + +void v8__WasmModuleCompilation__OnBytesReceived(v8::WasmModuleCompilation* self, + const uint8_t* bytes, + size_t size) { + self->OnBytesReceived(bytes, size); +} + +void v8__WasmModuleCompilation__Finish( + v8::WasmModuleCompilation* self, v8::Isolate* isolate, + void (*caching_callback)(v8::WasmStreaming::ModuleCachingInterface&), + void (*resolution_callback)(void* data, const v8::WasmModuleObject* module, + const v8::Value* error), + void* resolution_data, void (*drop_resolution_data)(void* data)) { + // Use shared_ptr to reference-count the Rust closure data through + // std::function's copy semantics. The custom deleter calls back into Rust + // to drop the boxed closure when the last copy is destroyed. + auto shared_data = std::shared_ptr( + resolution_data, + [drop_resolution_data](void* p) { drop_resolution_data(p); }); + self->Finish( + isolate, caching_callback, + [resolution_callback, shared_data](auto result) { + if (auto* module = + std::get_if>(&result)) { + resolution_callback(shared_data.get(), local_to_ptr(*module), + nullptr); + } else { + resolution_callback( + shared_data.get(), nullptr, + local_to_ptr(std::get>(result))); + } + }); +} + +void v8__WasmModuleCompilation__Abort(v8::WasmModuleCompilation* self) { + self->Abort(); +} + +void v8__WasmModuleCompilation__SetHasCompiledModuleBytes( + v8::WasmModuleCompilation* self) { + self->SetHasCompiledModuleBytes(); +} + +void v8__WasmModuleCompilation__SetMoreFunctionsCanBeSerializedCallback( + v8::WasmModuleCompilation* self, + void (*callback)(void* data, v8::CompiledWasmModule* compiled_module), + void* data, void (*drop_data)(void* data)) { + auto shared_data = + std::shared_ptr(data, [drop_data](void* p) { drop_data(p); }); + self->SetMoreFunctionsCanBeSerializedCallback( + [callback, shared_data](v8::CompiledWasmModule module) { + auto* heap_module = new v8::CompiledWasmModule(std::move(module)); + callback(shared_data.get(), heap_module); + }); +} + +void v8__WasmModuleCompilation__SetUrl(v8::WasmModuleCompilation* self, + const char* url, size_t length) { + self->SetUrl(url, length); +} + +} // extern "C" + // v8::CompiledWasmModule extern "C" { diff --git a/src/lib.rs b/src/lib.rs index 345cc9b7ae..305ea2cd3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,6 +185,7 @@ pub use value_serializer::ValueSerializerHelper; pub use value_serializer::ValueSerializerImpl; pub use wasm::CompiledWasmModule; pub use wasm::ModuleCachingInterface; +pub use wasm::WasmModuleCompilation; pub use wasm::WasmStreaming; /// https://v8.dev/docs/version-numbers diff --git a/src/wasm.rs b/src/wasm.rs index fe4fb5950a..2311d2bf4b 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,6 +1,11 @@ // Copyright 2019-2021 the Deno authors. All rights reserved. MIT license. +use std::ffi::c_void; +use std::ptr::null; +use std::ptr::null_mut; + use crate::ArrayBuffer; +use crate::Isolate; use crate::Local; use crate::PinScope; use crate::Value; @@ -18,8 +23,6 @@ use crate::support::Opaque; use crate::support::ToCFn; use crate::support::UnitType; use crate::support::char; -use std::ptr::null; -use std::ptr::null_mut; // Type-erased std::shared_ptr. Assumes it's safe // to move around (no backlinks). Not generally true for shared_ptrs @@ -275,6 +278,209 @@ impl Drop for CompiledWasmModule { } } +// Type-erased v8::WasmModuleCompilation allocated on the C++ heap. +#[repr(C)] +struct InternalWasmModuleCompilation(Opaque); + +/// An interface for asynchronous WebAssembly module compilation, to be used +/// e.g. for implementing source phase imports. +/// +/// Note: This interface is experimental and can change or be removed without +/// notice. +pub struct WasmModuleCompilation(*mut InternalWasmModuleCompilation); + +// OnBytesReceived can be called from any thread per V8 documentation. +unsafe impl Send for WasmModuleCompilation {} + +impl WasmModuleCompilation { + /// Start an asynchronous module compilation. This can be called on any + /// thread. + #[inline(always)] + pub fn new() -> Self { + unsafe { WasmModuleCompilation(v8__WasmModuleCompilation__NEW()) } + } + + /// Pass a new chunk of bytes to WebAssembly compilation. The buffer is + /// owned by the caller and will not be accessed after this call returns. + /// Can be called from any thread. + #[inline(always)] + pub fn on_bytes_received(&mut self, data: &[u8]) { + unsafe { + v8__WasmModuleCompilation__OnBytesReceived( + self.0, + data.as_ptr(), + data.len(), + ); + } + } + + /// Finish compilation. Must be called on the main thread after all bytes + /// were passed to [`Self::on_bytes_received`]. + /// + /// The `resolution_callback` will eventually be called with either the + /// compiled module or a compilation error. The callback receives `&Isolate` + /// so that [`crate::Global`] handles can be created from the [`Local`] + /// handles to persist them beyond the callback. + /// + /// Must not be called after [`Self::abort`]. + #[inline(always)] + pub fn finish( + self, + scope: &mut PinScope, + caching_callback: Option, + resolution_callback: impl FnOnce( + &Isolate, + Result, Local<'_, Value>>, + ) + 'static, + ) { + // Capture the isolate pointer in the closure so it doesn't need to be + // threaded through C++. + let isolate_ptr = scope.get_isolate_ptr(); + let wrapped = move |module: *const WasmModuleObject, + error: *const Value| { + let isolate = unsafe { Isolate::from_raw_ptr(isolate_ptr) }; + if !module.is_null() { + resolution_callback( + &isolate, + Ok(unsafe { Local::from_raw(module) }.unwrap()), + ); + } else { + resolution_callback( + &isolate, + Err(unsafe { Local::from_raw(error) }.unwrap()), + ); + } + }; + + // Double-box with Option: the outer Box gives us a thin pointer suitable + // for void*. The Option allows the trampoline to .take() the closure + // (FnOnce semantics) without freeing the outer allocation, which is + // ref-counted by shared_ptr on the C++ side. + #[allow(clippy::type_complexity)] + let boxed: Box< + Option>, + > = Box::new(Some(Box::new(wrapped))); + let data = Box::into_raw(boxed) as *mut c_void; + + unsafe { + v8__WasmModuleCompilation__Finish( + self.0, + scope.get_isolate_ptr(), + caching_callback, + resolution_trampoline, + data, + drop_resolution_data, + ); + } + } + + /// Abort compilation. Can be called from any thread. + /// Must not be called repeatedly, or after [`Self::finish`]. + #[inline(always)] + pub fn abort(self) { + unsafe { v8__WasmModuleCompilation__Abort(self.0) } + } + + /// Mark that the embedder has (potentially) cached compiled module bytes + /// (i.e. a serialized [`CompiledWasmModule`]) that could match this + /// compilation request. This will cause V8 to skip streaming compilation. + /// The embedder should then pass a caching callback to [`Self::finish`]. + #[inline(always)] + pub fn set_has_compiled_module_bytes(&mut self) { + unsafe { + v8__WasmModuleCompilation__SetHasCompiledModuleBytes(self.0); + } + } + + /// Sets a callback which is called whenever a significant number of new + /// functions are ready for serialization. + #[inline(always)] + pub fn set_more_functions_can_be_serialized_callback( + &mut self, + callback: impl Fn(CompiledWasmModule) + Send + 'static, + ) { + let boxed: Box> = + Box::new(Box::new(callback)); + let data = Box::into_raw(boxed) as *mut c_void; + + unsafe { + v8__WasmModuleCompilation__SetMoreFunctionsCanBeSerializedCallback( + self.0, + serialization_trampoline, + data, + drop_serialization_data, + ); + } + } + + /// Sets the UTF-8 encoded source URL for the `Script` object. This must + /// be called before [`Self::finish`]. + #[inline(always)] + pub fn set_url(&mut self, url: &str) { + // V8 requires the url to be null terminated. + let null_terminated_url = format!("{url}\0"); + unsafe { + v8__WasmModuleCompilation__SetUrl( + self.0, + null_terminated_url.as_ptr() as *const char, + url.len(), + ); + } + } +} + +impl Default for WasmModuleCompilation { + fn default() -> Self { + Self::new() + } +} + +impl Drop for WasmModuleCompilation { + fn drop(&mut self) { + unsafe { v8__WasmModuleCompilation__DELETE(self.0) } + } +} + +unsafe extern "C" fn resolution_trampoline( + data: *mut c_void, + module: *const WasmModuleObject, + error: *const Value, +) { + // Take the closure out of the Option without freeing the outer Box. + // The outer Box is ref-counted by shared_ptr on the C++ side and will + // be freed via drop_resolution_data when the last copy is destroyed. + let slot = unsafe { + &mut *(data + as *mut Option>) + }; + let callback = slot.take().unwrap(); + callback(module, error); +} + +unsafe extern "C" fn drop_resolution_data(data: *mut c_void) { + let _ = unsafe { + Box::from_raw( + data + as *mut Option>, + ) + }; +} + +unsafe extern "C" fn serialization_trampoline( + data: *mut c_void, + compiled_module: *mut InternalCompiledWasmModule, +) { + let callback = + unsafe { &**(data as *const Box) }; + callback(CompiledWasmModule(compiled_module)); +} + +unsafe extern "C" fn drop_serialization_data(data: *mut c_void) { + let _ = unsafe { + Box::from_raw(data as *mut Box) + }; +} + impl WasmMemoryObject { /// Returns underlying ArrayBuffer. #[inline(always)] @@ -380,4 +586,44 @@ unsafe extern "C" { fn v8__WasmMemoryObject__Buffer( this: *const WasmMemoryObject, ) -> *mut ArrayBuffer; + + fn v8__WasmModuleCompilation__NEW() -> *mut InternalWasmModuleCompilation; + fn v8__WasmModuleCompilation__DELETE( + this: *mut InternalWasmModuleCompilation, + ); + fn v8__WasmModuleCompilation__OnBytesReceived( + this: *mut InternalWasmModuleCompilation, + bytes: *const u8, + size: usize, + ); + fn v8__WasmModuleCompilation__Finish( + this: *mut InternalWasmModuleCompilation, + isolate: *mut RealIsolate, + caching_callback: Option, + resolution_callback: unsafe extern "C" fn( + *mut c_void, + *const WasmModuleObject, + *const Value, + ), + resolution_data: *mut c_void, + drop_resolution_data: unsafe extern "C" fn(*mut c_void), + ); + fn v8__WasmModuleCompilation__Abort(this: *mut InternalWasmModuleCompilation); + fn v8__WasmModuleCompilation__SetHasCompiledModuleBytes( + this: *mut InternalWasmModuleCompilation, + ); + fn v8__WasmModuleCompilation__SetMoreFunctionsCanBeSerializedCallback( + this: *mut InternalWasmModuleCompilation, + callback: unsafe extern "C" fn( + *mut c_void, + *mut InternalCompiledWasmModule, + ), + data: *mut c_void, + drop_data: unsafe extern "C" fn(*mut c_void), + ); + fn v8__WasmModuleCompilation__SetUrl( + this: *mut InternalWasmModuleCompilation, + url: *const char, + length: usize, + ); } diff --git a/tests/test_api.rs b/tests/test_api.rs index 02b0b814b2..ecf12bd2fc 100644 --- a/tests/test_api.rs +++ b/tests/test_api.rs @@ -8782,6 +8782,60 @@ fn wasm_streaming_callback() { assert!(global.get(scope, name).unwrap().strict_equals(exception)); } +#[test] +fn wasm_module_compilation() { + let _setup_guard = setup::parallel_test(); + + let isolate = &mut v8::Isolate::new(v8::CreateParams::default()); + v8::scope!(let scope, isolate); + + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + + // Start compilation. + let mut compilation = v8::WasmModuleCompilation::new(); + + // MVP of WASM modules: magic marker + version 1. + compilation + .on_bytes_received(&[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]); + + compilation.set_url("https://example.com/test.wasm"); + + // Finish compilation. + let result: Rc>>> = + Rc::new(RefCell::new(None)); + let result_clone = result.clone(); + compilation.finish(scope, None, move |isolate, r| match r { + Ok(module) => { + result_clone + .borrow_mut() + .replace(v8::Global::new(isolate, module)); + } + Err(_) => panic!("wasm compilation failed"), + }); + + // Execute pending tasks. + while v8::Platform::pump_message_loop( + &v8::V8::get_current_platform(), + scope, + false, + ) {} + + let global_module = result.borrow_mut().take(); + assert!(global_module.is_some()); +} + +#[test] +fn wasm_module_compilation_abort() { + let _setup_guard = setup::parallel_test(); + + // Start compilation and abort it. + let mut compilation = v8::WasmModuleCompilation::new(); + compilation + .on_bytes_received(&[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]); + compilation.abort(); +} + #[test] fn unbound_script_conversion() { let _setup_guard = setup::parallel_test();