deno/ops/fast_call.rs
Divy Srivastava 38555a6a0f
feat(ops): reland fast zero copy string arguments (#17996)
Reland https://github.com/denoland/deno/pull/16777

The codegen is disabled in async ops and when fallback to slow call is
possible (return type is a Result) to avoid hitting this V8 bug:
https://github.com/denoland/deno/issues/17159
2023-03-03 19:04:10 +05:30

476 lines
13 KiB
Rust

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
//! Code generation for V8 fast calls.
use pmutil::q;
use pmutil::Quote;
use pmutil::ToTokensExt;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse_quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::GenericParam;
use syn::Generics;
use syn::Ident;
use syn::ItemFn;
use syn::ItemImpl;
use syn::Path;
use syn::PathArguments;
use syn::PathSegment;
use syn::Type;
use syn::TypePath;
use crate::optimizer::FastValue;
use crate::optimizer::Optimizer;
pub(crate) struct FastImplItems {
pub(crate) impl_and_fn: TokenStream,
pub(crate) decl: TokenStream,
pub(crate) active: bool,
}
pub(crate) fn generate(
core: &TokenStream,
optimizer: &mut Optimizer,
item_fn: &ItemFn,
) -> FastImplItems {
if !optimizer.fast_compatible {
return FastImplItems {
impl_and_fn: TokenStream::new(),
decl: quote! { None },
active: false,
};
}
// TODO(@littledivy): Use `let..else` on 1.65.0
let output_ty = match &optimizer.fast_result {
// Assert that the optimizer did not set a return type.
//
// @littledivy: This *could* potentially be used to optimize resolving
// promises but knowing the return type at compile time instead of
// serde_v8 serialization.
Some(_) if optimizer.is_async => &FastValue::Void,
Some(ty) => ty,
None if optimizer.is_async => &FastValue::Void,
None => {
return FastImplItems {
impl_and_fn: TokenStream::new(),
decl: quote! { None },
active: false,
}
}
};
// We've got 3 idents.
//
// - op_foo, the public op declaration contains the user function.
// - op_foo_fast, the fast call type.
// - op_foo_fast_fn, the fast call function.
let ident = item_fn.sig.ident.clone();
let fast_ident = Ident::new(&format!("{ident}_fast"), Span::call_site());
let fast_fn_ident =
Ident::new(&format!("{ident}_fast_fn"), Span::call_site());
// Deal with generics.
let generics = &item_fn.sig.generics;
let (impl_generics, _, where_clause) = generics.split_for_impl();
// struct op_foo_fast <T, U> { ... }
let struct_generics = exclude_lifetime_params(&generics.params);
// std::marker::PhantomData <A>
let phantom_generics: Quote = match struct_generics {
Some(ref params) => q!(Vars { params }, { params }),
None => q!({ <()> }),
};
// op_foo_fast_fn :: <T>
let caller_generics: Quote = match struct_generics {
Some(ref params) => q!(Vars { params }, { ::params }),
None => q!({}),
};
// This goes in the FastFunction impl block.
let mut segments = Punctuated::new();
{
let mut arguments = PathArguments::None;
if let Some(ref struct_generics) = struct_generics {
arguments = PathArguments::AngleBracketed(parse_quote! {
#struct_generics
});
}
segments.push_value(PathSegment {
ident: fast_ident.clone(),
arguments,
});
}
// struct T <A> {
// _phantom: ::std::marker::PhantomData<A>,
// }
let fast_ty: Quote = q!(Vars { Type: &fast_ident, generics: &struct_generics, phantom_generics }, {
struct Type generics {
_phantom: ::std::marker::PhantomData phantom_generics,
}
});
// Original inputs.
let mut inputs = item_fn.sig.inputs.clone();
let mut transforms = q!({});
let mut pre_transforms = q!({});
// Apply parameter transforms
for (index, input) in inputs.iter_mut().enumerate() {
if let Some(transform) = optimizer.transforms.get(&index) {
let quo: Quote = transform.apply_for_fast_call(core, input);
transforms.push_tokens(&quo);
}
}
// Collect idents to be passed into function call, we can now freely
// modify the inputs.
let idents = inputs
.iter()
.map(|input| match input {
syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
syn::Pat::Ident(pat_ident) => pat_ident.ident.clone(),
_ => panic!("unexpected pattern"),
},
_ => panic!("unexpected argument"),
})
.collect::<Punctuated<_, Comma>>();
// Retain only *pure* parameters.
let mut fast_fn_inputs = if optimizer.has_opstate_in_parameters() {
inputs.into_iter().skip(1).collect()
} else {
inputs
};
let mut input_variants = optimizer
.fast_parameters
.iter()
.map(q_fast_ty_variant)
.collect::<Punctuated<_, Comma>>();
// Apply *hard* optimizer hints.
if optimizer.has_fast_callback_option
|| optimizer.has_wasm_memory
|| optimizer.needs_opstate()
|| optimizer.is_async
|| optimizer.needs_fast_callback_option
{
let decl = parse_quote! {
fast_api_callback_options: *mut #core::v8::fast_api::FastApiCallbackOptions
};
if optimizer.has_fast_callback_option || optimizer.has_wasm_memory {
// Replace last parameter.
assert!(fast_fn_inputs.pop().is_some());
fast_fn_inputs.push(decl);
} else {
fast_fn_inputs.push(decl);
}
input_variants.push(q!({ CallbackOptions }));
}
// (recv, p_id, ...)
//
// Optimizer has already set it in the fast parameter variant list.
if optimizer.is_async {
if fast_fn_inputs.is_empty() {
fast_fn_inputs.push(parse_quote! { __promise_id: i32 });
} else {
fast_fn_inputs.insert(0, parse_quote! { __promise_id: i32 });
}
}
let mut output_transforms = q!({});
if optimizer.needs_opstate()
|| optimizer.is_async
|| optimizer.has_fast_callback_option
|| optimizer.has_wasm_memory
{
// Dark arts 🪄 ✨
//
// - V8 calling convention guarantees that the callback options pointer is non-null.
// - `data` union is always initialized as the `v8::Local<v8::Value>` variant.
// - deno_core guarantees that `data` is a v8 External pointing to an OpCtx for the
// isolate's lifetime.
let prelude = q!({
let __opts: &mut v8::fast_api::FastApiCallbackOptions =
unsafe { &mut *fast_api_callback_options };
});
pre_transforms.push_tokens(&prelude);
}
if optimizer.needs_opstate() || optimizer.is_async {
// Grab the op_state identifier, the first one. ¯\_(ツ)_/¯
let op_state = match idents.first() {
Some(ident) if optimizer.has_opstate_in_parameters() => ident.clone(),
// fn op_foo() -> Result<...>
_ => Ident::new("op_state", Span::call_site()),
};
let ctx = q!({
let __ctx = unsafe {
&*(v8::Local::<v8::External>::cast(unsafe { __opts.data.data }).value()
as *const _ops::OpCtx)
};
});
pre_transforms.push_tokens(&ctx);
pre_transforms.push_tokens(&match optimizer.is_async {
false => q!(
Vars {
op_state: &op_state
},
{
let op_state = &mut ::std::cell::RefCell::borrow_mut(&__ctx.state);
}
),
true => q!(
Vars {
op_state: &op_state
},
{
let op_state = __ctx.state.clone();
}
),
});
if optimizer.returns_result && !optimizer.is_async {
// Magic fallback 🪄
//
// If Result<T, E> is Ok(T), return T as fast value.
//
// Err(E) gets put into `last_fast_op_error` slot and
//
// V8 calls the slow path so we can take the slot
// value and throw.
let default = optimizer.fast_result.as_ref().unwrap().default_value();
let result_wrap = q!(Vars { op_state, default }, {
match result {
Ok(result) => result,
Err(err) => {
op_state.last_fast_op_error.replace(err);
__opts.fallback = true;
default
}
}
});
output_transforms.push_tokens(&result_wrap);
}
}
if optimizer.is_async {
// Referenced variables are declared in parent block.
let track_async = q!({
let __op_id = __ctx.id;
let __state = ::std::cell::RefCell::borrow(&__ctx.state);
__state.tracker.track_async(__op_id);
});
output_transforms.push_tokens(&track_async);
let queue_future = if optimizer.returns_result {
q!({
let realm_idx = __ctx.realm_idx;
let __get_class = __state.get_error_class_fn;
let result = _ops::queue_fast_async_op(__ctx, async move {
let result = result.await;
(
realm_idx,
__promise_id,
__op_id,
_ops::to_op_result(__get_class, result),
)
});
})
} else {
q!({
let realm_idx = __ctx.realm_idx;
let result = _ops::queue_fast_async_op(__ctx, async move {
let result = result.await;
(
realm_idx,
__promise_id,
__op_id,
_ops::OpResult::Ok(result.into()),
)
});
})
};
output_transforms.push_tokens(&queue_future);
}
if !optimizer.returns_result {
let default_output = q!({ result });
output_transforms.push_tokens(&default_output);
}
let output = q_fast_ty(output_ty);
// Generate the function body.
//
// fn f <S> (_: Local<Object>, a: T, b: U) -> R {
// /* Transforms */
// let a = a.into();
// let b = b.into();
//
// let r = op::call(a, b);
//
// /* Return transform */
// r.into()
// }
let fast_fn = q!(
Vars { core, pre_transforms, op_name_fast: &fast_fn_ident, op_name: &ident, fast_fn_inputs, generics, call_generics: &caller_generics, where_clause, idents, transforms, output_transforms, output: &output },
{
fn op_name_fast generics (_: core::v8::Local<core::v8::Object>, fast_fn_inputs) -> output where_clause {
use core::v8;
use core::_ops;
pre_transforms
transforms
let result = op_name::call call_generics (idents);
output_transforms
}
}
);
let output_variant = q_fast_ty_variant(output_ty);
let mut generics: Generics = parse_quote! { #impl_generics };
generics.where_clause = where_clause.cloned();
// impl <A> fast_api::FastFunction for T <A> where A: B {
// fn function(&self) -> *const ::std::ffi::c_void {
// f as *const ::std::ffi::c_void
// }
// fn args(&self) -> &'static [fast_api::Type] {
// &[ CType::T, CType::U ]
// }
// fn return_type(&self) -> fast_api::CType {
// CType::T
// }
// }
let item: ItemImpl = ItemImpl {
attrs: vec![],
defaultness: None,
unsafety: None,
impl_token: Default::default(),
generics,
trait_: Some((
None,
parse_quote!(#core::v8::fast_api::FastFunction),
Default::default(),
)),
self_ty: Box::new(Type::Path(TypePath {
qself: None,
path: Path {
leading_colon: None,
segments,
},
})),
brace_token: Default::default(),
items: vec![
parse_quote! {
fn function(&self) -> *const ::std::ffi::c_void {
#fast_fn_ident #caller_generics as *const ::std::ffi::c_void
}
},
parse_quote! {
fn args(&self) -> &'static [#core::v8::fast_api::Type] {
use #core::v8::fast_api::Type::*;
use #core::v8::fast_api::CType;
&[ #input_variants ]
}
},
parse_quote! {
fn return_type(&self) -> #core::v8::fast_api::CType {
#core::v8::fast_api::CType::#output_variant
}
},
],
};
let mut tts = q!({});
tts.push_tokens(&fast_ty);
tts.push_tokens(&item);
tts.push_tokens(&fast_fn);
let impl_and_fn = tts.dump();
let decl = q!(
Vars { fast_ident, caller_generics },
{
Some(Box::new(fast_ident caller_generics { _phantom: ::std::marker::PhantomData }))
}
).dump();
FastImplItems {
impl_and_fn,
decl,
active: true,
}
}
/// Quote fast value type.
fn q_fast_ty(v: &FastValue) -> Quote {
match v {
FastValue::Void => q!({ () }),
FastValue::Bool => q!({ bool }),
FastValue::U32 => q!({ u32 }),
FastValue::I32 => q!({ i32 }),
FastValue::U64 => q!({ u64 }),
FastValue::I64 => q!({ i64 }),
FastValue::F32 => q!({ f32 }),
FastValue::F64 => q!({ f64 }),
FastValue::Pointer => q!({ *mut ::std::ffi::c_void }),
FastValue::V8Value => q!({ v8::Local<v8::Value> }),
FastValue::Uint8Array
| FastValue::Uint32Array
| FastValue::Float64Array
| FastValue::SeqOneByteString => unreachable!(),
}
}
/// Quote fast value type's variant.
fn q_fast_ty_variant(v: &FastValue) -> Quote {
match v {
FastValue::Void => q!({ Void }),
FastValue::Bool => q!({ Bool }),
FastValue::U32 => q!({ Uint32 }),
FastValue::I32 => q!({ Int32 }),
FastValue::U64 => q!({ Uint64 }),
FastValue::I64 => q!({ Int64 }),
FastValue::F32 => q!({ Float32 }),
FastValue::F64 => q!({ Float64 }),
FastValue::Pointer => q!({ Pointer }),
FastValue::V8Value => q!({ V8Value }),
FastValue::Uint8Array => q!({ TypedArray(CType::Uint8) }),
FastValue::Uint32Array => q!({ TypedArray(CType::Uint32) }),
FastValue::Float64Array => q!({ TypedArray(CType::Float64) }),
FastValue::SeqOneByteString => q!({ SeqOneByteString }),
}
}
fn exclude_lifetime_params(
generic_params: &Punctuated<GenericParam, Comma>,
) -> Option<Generics> {
let params = generic_params
.iter()
.filter(|t| !matches!(t, GenericParam::Lifetime(_)))
.cloned()
.collect::<Punctuated<GenericParam, Comma>>();
if params.is_empty() {
// <()>
return None;
}
Some(Generics {
lt_token: Some(Default::default()),
params,
gt_token: Some(Default::default()),
where_clause: None,
})
}