feat(ops): fast calls for Wasm (#16776)

This PR introduces Wasm ops. These calls are optimized for entry from
Wasm land.

The `#[op(wasm)]` attribute is opt-in. 

Last parameter `Option<&mut [u8]>` is the memory slice of the Wasm
module *when entered from a Fast API call*. Otherwise, the user is
expected to implement logic to obtain the memory if `None`

```rust
#[op(wasm)]
pub fn op_args_get(
  offset: i32,
  buffer_offset: i32,
  memory: Option<&mut [u8]>,
) {
  // ...
}
```
This commit is contained in:
Divy Srivastava 2022-11-27 05:54:28 -08:00 committed by GitHub
parent 9ffc6acdbb
commit ca66978a5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 291 additions and 8 deletions

28
core/examples/wasm.js Normal file
View file

@ -0,0 +1,28 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
// asc wasm.ts --exportStart --initialMemory 6400 -O -o wasm.wasm
// deno-fmt-ignore
const bytes = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2,
15, 1, 3, 111, 112, 115, 7, 111, 112, 95, 119, 97, 115, 109, 0,
0, 3, 3, 2, 0, 0, 5, 4, 1, 0, 128, 50, 7, 36, 4,
7, 111, 112, 95, 119, 97, 115, 109, 0, 0, 4, 99, 97, 108, 108,
0, 1, 6, 109, 101, 109, 111, 114, 121, 2, 0, 6, 95, 115, 116,
97, 114, 116, 0, 2, 10, 10, 2, 4, 0, 16, 0, 11, 3, 0,
1, 11
]);
const { ops } = Deno.core;
const module = new WebAssembly.Module(bytes);
const instance = new WebAssembly.Instance(module, { ops });
ops.op_set_wasm_mem(instance.exports.memory);
instance.exports.call();
const memory = instance.exports.memory;
const view = new Uint8Array(memory.buffer);
if (view[0] !== 69) {
throw new Error("Expected first byte to be 69");
}

67
core/examples/wasm.rs Normal file
View file

@ -0,0 +1,67 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use deno_core::op;
use deno_core::Extension;
use deno_core::JsRuntime;
use deno_core::RuntimeOptions;
use std::mem::transmute;
use std::ptr::NonNull;
// This is a hack to make the `#[op]` macro work with
// deno_core examples.
// You can remove this:
use deno_core::*;
struct WasmMemory(NonNull<v8::WasmMemoryObject>);
fn wasm_memory_unchecked(state: &mut OpState) -> &mut [u8] {
let WasmMemory(global) = state.borrow::<WasmMemory>();
// SAFETY: `v8::Local` is always non-null pointer; the `HandleScope` is
// already on the stack, but we don't have access to it.
let memory_object = unsafe {
transmute::<NonNull<v8::WasmMemoryObject>, v8::Local<v8::WasmMemoryObject>>(
*global,
)
};
let backing_store = memory_object.buffer().get_backing_store();
let ptr = backing_store.data().unwrap().as_ptr() as *mut u8;
let len = backing_store.byte_length();
// SAFETY: `ptr` is a valid pointer to `len` bytes.
unsafe { std::slice::from_raw_parts_mut(ptr, len) }
}
#[op(wasm)]
fn op_wasm(state: &mut OpState, memory: Option<&mut [u8]>) {
let memory = memory.unwrap_or_else(|| wasm_memory_unchecked(state));
memory[0] = 69;
}
#[op(v8)]
fn op_set_wasm_mem(
scope: &mut v8::HandleScope,
state: &mut OpState,
memory: serde_v8::Value,
) {
let memory =
v8::Local::<v8::WasmMemoryObject>::try_from(memory.v8_value).unwrap();
let global = v8::Global::new(scope, memory);
state.put(WasmMemory(global.into_raw()));
}
fn main() {
// Build a deno_core::Extension providing custom ops
let ext = Extension::builder()
.ops(vec![op_wasm::decl(), op_set_wasm_mem::decl()])
.build();
// Initialize a runtime instance
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![ext],
..Default::default()
});
runtime
.execute_script("<usage>", include_str!("wasm.js"))
.unwrap();
}

7
core/examples/wasm.ts Normal file
View file

@ -0,0 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
export declare function op_wasm(): void;
export function call(): void {
op_wasm();
}

View file

@ -34,8 +34,8 @@ Cases where code is optimized away:
The macro will infer and try to auto generate V8 fast API call trait impl for The macro will infer and try to auto generate V8 fast API call trait impl for
`sync` ops with: `sync` ops with:
- arguments: integers, bool, `&mut OpState`, `&[u8]`, &mut [u8]`,`&[u32]`,`&mut - arguments: integers, bool, `&mut OpState`, `&[u8]`, `&mut [u8]`, `&[u32]`,
[u32]` `&mut [u32]`
- return_type: integers, bool - return_type: integers, bool
The `#[op(fast)]` attribute should be used to enforce fast call generation at The `#[op(fast)]` attribute should be used to enforce fast call generation at
@ -43,3 +43,20 @@ compile time.
Trait gen for `async` ops & a ZeroCopyBuf equivalent type is planned and will be Trait gen for `async` ops & a ZeroCopyBuf equivalent type is planned and will be
added soon. added soon.
### Wasm calls
The `#[op(wasm)]` attribute should be used for calls expected to be called from
Wasm. This enables the fast call generation and allows seamless `WasmMemory`
integration for generic and fast calls.
```rust
#[op(wasm)]
pub fn op_args_get(
offset: i32,
buffer_offset: i32,
memory: Option<&[u8]>, // Must be last parameter. Some(..) when entered from Wasm.
) {
// ...
}
```

View file

@ -11,6 +11,7 @@ pub struct Attributes {
pub is_v8: bool, pub is_v8: bool,
pub must_be_fast: bool, pub must_be_fast: bool,
pub deferred: bool, pub deferred: bool,
pub is_wasm: bool,
} }
impl Parse for Attributes { impl Parse for Attributes {
@ -20,18 +21,22 @@ impl Parse for Attributes {
let vars: Vec<_> = vars.iter().map(Ident::to_string).collect(); let vars: Vec<_> = vars.iter().map(Ident::to_string).collect();
let vars: Vec<_> = vars.iter().map(String::as_str).collect(); let vars: Vec<_> = vars.iter().map(String::as_str).collect();
for var in vars.iter() { for var in vars.iter() {
if !["unstable", "v8", "fast", "deferred"].contains(var) { if !["unstable", "v8", "fast", "deferred", "wasm"].contains(var) {
return Err(Error::new( return Err(Error::new(
input.span(), input.span(),
"invalid attribute, expected one of: unstable, v8, fast, deferred", "invalid attribute, expected one of: unstable, v8, fast, deferred, wasm",
)); ));
} }
} }
let is_wasm = vars.contains(&"wasm");
Ok(Self { Ok(Self {
is_unstable: vars.contains(&"unstable"), is_unstable: vars.contains(&"unstable"),
is_v8: vars.contains(&"v8"), is_v8: vars.contains(&"v8"),
must_be_fast: vars.contains(&"fast"),
deferred: vars.contains(&"deferred"), deferred: vars.contains(&"deferred"),
must_be_fast: is_wasm || vars.contains(&"fast"),
is_wasm,
}) })
} }
} }

View file

@ -139,6 +139,7 @@ pub(crate) fn generate(
// Apply *hard* optimizer hints. // Apply *hard* optimizer hints.
if optimizer.has_fast_callback_option if optimizer.has_fast_callback_option
|| optimizer.has_wasm_memory
|| optimizer.needs_opstate() || optimizer.needs_opstate()
|| optimizer.is_async || optimizer.is_async
|| optimizer.needs_fast_callback_option || optimizer.needs_fast_callback_option
@ -147,7 +148,7 @@ pub(crate) fn generate(
fast_api_callback_options: *mut #core::v8::fast_api::FastApiCallbackOptions fast_api_callback_options: *mut #core::v8::fast_api::FastApiCallbackOptions
}; };
if optimizer.has_fast_callback_option { if optimizer.has_fast_callback_option || optimizer.has_wasm_memory {
// Replace last parameter. // Replace last parameter.
assert!(fast_fn_inputs.pop().is_some()); assert!(fast_fn_inputs.pop().is_some());
fast_fn_inputs.push(decl); fast_fn_inputs.push(decl);
@ -174,6 +175,7 @@ pub(crate) fn generate(
if optimizer.needs_opstate() if optimizer.needs_opstate()
|| optimizer.is_async || optimizer.is_async
|| optimizer.has_fast_callback_option || optimizer.has_fast_callback_option
|| optimizer.has_wasm_memory
{ {
// Dark arts 🪄 ✨ // Dark arts 🪄 ✨
// //

View file

@ -386,7 +386,9 @@ fn codegen_arg(
let ident = quote::format_ident!("{name}"); let ident = quote::format_ident!("{name}");
let (pat, ty) = match arg { let (pat, ty) = match arg {
syn::FnArg::Typed(pat) => { syn::FnArg::Typed(pat) => {
if is_optional_fast_callback_option(&pat.ty) { if is_optional_fast_callback_option(&pat.ty)
|| is_optional_wasm_memory(&pat.ty)
{
return quote! { let #ident = None; }; return quote! { let #ident = None; };
} }
(&pat.pat, &pat.ty) (&pat.pat, &pat.ty)
@ -663,6 +665,10 @@ fn is_optional_fast_callback_option(ty: impl ToTokens) -> bool {
tokens(&ty).contains("Option < & mut FastApiCallbackOptions") tokens(&ty).contains("Option < & mut FastApiCallbackOptions")
} }
fn is_optional_wasm_memory(ty: impl ToTokens) -> bool {
tokens(&ty).contains("Option < & mut [u8]")
}
/// Detects if the type can be set using `rv.set_uint32` fast path /// Detects if the type can be set using `rv.set_uint32` fast path
fn is_u32_rv(ty: impl ToTokens) -> bool { fn is_u32_rv(ty: impl ToTokens) -> bool {
["u32", "u8", "u16"].iter().any(|&s| tokens(&ty) == s) || is_resource_id(&ty) ["u32", "u8", "u16"].iter().any(|&s| tokens(&ty) == s) || is_resource_id(&ty)
@ -743,6 +749,10 @@ mod tests {
if source.contains("// @test-attr:fast") { if source.contains("// @test-attr:fast") {
attrs.must_be_fast = true; attrs.must_be_fast = true;
} }
if source.contains("// @test-attr:wasm") {
attrs.is_wasm = true;
attrs.must_be_fast = true;
}
let item = syn::parse_str(&source).expect("Failed to parse test file"); let item = syn::parse_str(&source).expect("Failed to parse test file");
let op = Op::new(item, attrs); let op = Op::new(item, attrs);

View file

@ -26,6 +26,7 @@ enum TransformKind {
SliceU32(bool), SliceU32(bool),
SliceU8(bool), SliceU8(bool),
PtrU8, PtrU8,
WasmMemory,
} }
impl Transform { impl Transform {
@ -50,6 +51,13 @@ impl Transform {
} }
} }
fn wasm_memory(index: usize) -> Self {
Transform {
kind: TransformKind::WasmMemory,
index,
}
}
fn u8_ptr(index: usize) -> Self { fn u8_ptr(index: usize) -> Self {
Transform { Transform {
kind: TransformKind::PtrU8, kind: TransformKind::PtrU8,
@ -124,6 +132,16 @@ impl Transform {
}; };
}) })
} }
TransformKind::WasmMemory => {
// Note: `ty` is correctly set to __opts by the fast call tier.
q!(Vars { var: &ident, core }, {
let var = unsafe {
&*(__opts.wasm_memory
as *const core::v8::fast_api::FastApiTypedArray<u8>)
}
.get_storage_if_aligned();
})
}
// *const u8 // *const u8
TransformKind::PtrU8 => { TransformKind::PtrU8 => {
*ty = *ty =
@ -201,6 +219,8 @@ pub(crate) struct Optimizer {
// Do we depend on FastApiCallbackOptions? // Do we depend on FastApiCallbackOptions?
pub(crate) needs_fast_callback_option: bool, pub(crate) needs_fast_callback_option: bool,
pub(crate) has_wasm_memory: bool,
pub(crate) fast_result: Option<FastValue>, pub(crate) fast_result: Option<FastValue>,
pub(crate) fast_parameters: Vec<FastValue>, pub(crate) fast_parameters: Vec<FastValue>,
@ -262,6 +282,9 @@ impl Optimizer {
self.is_async = op.is_async; self.is_async = op.is_async;
self.fast_compatible = true; self.fast_compatible = true;
// Just assume for now. We will validate later.
self.has_wasm_memory = op.attrs.is_wasm;
let sig = &op.item.sig; let sig = &op.item.sig;
// Analyze return type // Analyze return type
@ -419,7 +442,32 @@ impl Optimizer {
TypeReference { elem, .. }, TypeReference { elem, .. },
))) = args.last() ))) = args.last()
{ {
if let Type::Path(TypePath { if self.has_wasm_memory {
// -> Option<&mut [u8]>
if let Type::Slice(TypeSlice { elem, .. }) = &**elem {
if let Type::Path(TypePath {
path: Path { segments, .. },
..
}) = &**elem
{
let segment = single_segment(segments)?;
match segment {
// Is `T` a u8?
PathSegment { ident, .. } if ident == "u8" => {
self.needs_fast_callback_option = true;
assert!(self
.transforms
.insert(index, Transform::wasm_memory(index))
.is_none());
}
_ => {
return Err(BailoutReason::FastUnsupportedParamType)
}
}
}
}
} else if let Type::Path(TypePath {
path: Path { segments, .. }, path: Path { segments, .. },
.. ..
}) = &**elem }) = &**elem
@ -654,6 +702,10 @@ mod tests {
.expect("Failed to read expected file"); .expect("Failed to read expected file");
let mut attrs = Attributes::default(); let mut attrs = Attributes::default();
if source.contains("// @test-attr:wasm") {
attrs.must_be_fast = true;
attrs.is_wasm = true;
}
if source.contains("// @test-attr:fast") { if source.contains("// @test-attr:fast") {
attrs.must_be_fast = true; attrs.must_be_fast = true;
} }

View file

@ -0,0 +1,11 @@
=== Optimizer Dump ===
returns_result: false
has_ref_opstate: false
has_rc_opstate: false
has_fast_callback_option: false
needs_fast_callback_option: true
fast_result: Some(Void)
fast_parameters: [V8Value]
transforms: {0: Transform { kind: WasmMemory, index: 0 }}
is_async: false
fast_compatible: true

View file

@ -0,0 +1,81 @@
#[allow(non_camel_case_types)]
///Auto-generated by `deno_ops`, i.e: `#[op]`
///
///Use `op_wasm::decl()` to get an op-declaration
///you can include in a `deno_core::Extension`.
pub struct op_wasm;
#[doc(hidden)]
impl op_wasm {
pub fn name() -> &'static str {
stringify!(op_wasm)
}
pub fn v8_fn_ptr<'scope>() -> deno_core::v8::FunctionCallback {
use deno_core::v8::MapFnTo;
Self::v8_func.map_fn_to()
}
pub fn decl<'scope>() -> deno_core::OpDecl {
deno_core::OpDecl {
name: Self::name(),
v8_fn_ptr: Self::v8_fn_ptr(),
enabled: true,
fast_fn: Some(
Box::new(op_wasm_fast {
_phantom: ::std::marker::PhantomData,
}),
),
is_async: false,
is_unstable: false,
is_v8: false,
argc: 1usize,
}
}
#[inline]
#[allow(clippy::too_many_arguments)]
fn call(memory: Option<&mut [u8]>) {}
pub fn v8_func<'scope>(
scope: &mut deno_core::v8::HandleScope<'scope>,
args: deno_core::v8::FunctionCallbackArguments,
mut rv: deno_core::v8::ReturnValue,
) {
let ctx = unsafe {
&*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
as *const deno_core::_ops::OpCtx)
};
let arg_0 = None;
let result = Self::call(arg_0);
let op_state = ::std::cell::RefCell::borrow(&*ctx.state);
op_state.tracker.track_sync(ctx.id);
}
}
struct op_wasm_fast {
_phantom: ::std::marker::PhantomData<()>,
}
impl<'scope> deno_core::v8::fast_api::FastFunction for op_wasm_fast {
fn function(&self) -> *const ::std::ffi::c_void {
op_wasm_fast_fn as *const ::std::ffi::c_void
}
fn args(&self) -> &'static [deno_core::v8::fast_api::Type] {
use deno_core::v8::fast_api::Type::*;
use deno_core::v8::fast_api::CType;
&[V8Value, CallbackOptions]
}
fn return_type(&self) -> deno_core::v8::fast_api::CType {
deno_core::v8::fast_api::CType::Void
}
}
fn op_wasm_fast_fn<'scope>(
_: deno_core::v8::Local<deno_core::v8::Object>,
fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions,
) -> () {
use deno_core::v8;
use deno_core::_ops;
let __opts: &mut v8::fast_api::FastApiCallbackOptions = unsafe {
&mut *fast_api_callback_options
};
let memory = unsafe {
&*(__opts.wasm_memory as *const deno_core::v8::fast_api::FastApiTypedArray<u8>)
}
.get_storage_if_aligned();
let result = op_wasm::call(memory);
result
}

View file

@ -0,0 +1,3 @@
fn op_wasm(memory: Option<&mut [u8]>) {
// @test-attr:wasm
}