1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-28 18:19:08 -05:00
denoland-deno/ops/fast_call.rs

480 lines
13 KiB
Rust
Raw Normal View History

// 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() {
2023-01-16 15:27:41 -05:00
inputs.into_iter().skip(1).collect()
} else {
2023-01-16 15:27:41 -05:00
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!({
feat(core): Reland support for async ops in realms (#17204) Currently realms are supported on `deno_core`, but there was no support for async ops anywhere other than the main realm. The main issue is that the `js_recv_cb` callback, which resolves promises corresponding to async ops, was only set for the main realm, so async ops in other realms would never resolve. Furthermore, promise ID's are specific to each realm, which meant that async ops from other realms would result in a wrong promise from the main realm being resolved. This change takes the `ContextState` struct added in #17050, and adds to it a `js_recv_cb` callback for each realm. Combined with the fact that that same PR also added a list of known realms to `JsRuntimeState`, and that #17174 made `OpCtx` instances realm-specific and had them include an index into that list of known realms, this makes it possible to know the current realm in the `queue_async_op` and `queue_fast_async_op` methods, and therefore to send the results of promises for each realm to that realm, and prevent the ID's from getting mixed up. Additionally, since promise ID's are no longer unique to the isolate, having a single set of unrefed ops doesn't work. This change therefore also moves `unrefed_ops` from `JsRuntimeState` to `ContextState`, and adds the lengths of the unrefed op sets for all known realms to get the total number of unrefed ops to compare in the event loop. This PR is a reland of #14734 after it was reverted in #16366, except that `ContextState` and `JsRuntimeState::known_realms` were previously relanded in #17050. Another significant difference with the original PR is passing around an index into `JsRuntimeState::known_realms` instead of a `v8::Global<v8::Context>` to identify the realm, because async op queuing in fast calls cannot call into V8, and therefore cannot have access to V8 globals. This also simplified the implementation of `resolve_async_ops`. Co-authored-by: Luis Malheiro <luismalheiro@gmail.com>
2023-01-14 08:40:16 -05:00
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;
(
feat(core): Reland support for async ops in realms (#17204) Currently realms are supported on `deno_core`, but there was no support for async ops anywhere other than the main realm. The main issue is that the `js_recv_cb` callback, which resolves promises corresponding to async ops, was only set for the main realm, so async ops in other realms would never resolve. Furthermore, promise ID's are specific to each realm, which meant that async ops from other realms would result in a wrong promise from the main realm being resolved. This change takes the `ContextState` struct added in #17050, and adds to it a `js_recv_cb` callback for each realm. Combined with the fact that that same PR also added a list of known realms to `JsRuntimeState`, and that #17174 made `OpCtx` instances realm-specific and had them include an index into that list of known realms, this makes it possible to know the current realm in the `queue_async_op` and `queue_fast_async_op` methods, and therefore to send the results of promises for each realm to that realm, and prevent the ID's from getting mixed up. Additionally, since promise ID's are no longer unique to the isolate, having a single set of unrefed ops doesn't work. This change therefore also moves `unrefed_ops` from `JsRuntimeState` to `ContextState`, and adds the lengths of the unrefed op sets for all known realms to get the total number of unrefed ops to compare in the event loop. This PR is a reland of #14734 after it was reverted in #16366, except that `ContextState` and `JsRuntimeState::known_realms` were previously relanded in #17050. Another significant difference with the original PR is passing around an index into `JsRuntimeState::known_realms` instead of a `v8::Global<v8::Context>` to identify the realm, because async op queuing in fast calls cannot call into V8, and therefore cannot have access to V8 globals. This also simplified the implementation of `resolve_async_ops`. Co-authored-by: Luis Malheiro <luismalheiro@gmail.com>
2023-01-14 08:40:16 -05:00
realm_idx,
__promise_id,
__op_id,
_ops::to_op_result(__get_class, result),
)
});
})
} else {
q!({
feat(core): Reland support for async ops in realms (#17204) Currently realms are supported on `deno_core`, but there was no support for async ops anywhere other than the main realm. The main issue is that the `js_recv_cb` callback, which resolves promises corresponding to async ops, was only set for the main realm, so async ops in other realms would never resolve. Furthermore, promise ID's are specific to each realm, which meant that async ops from other realms would result in a wrong promise from the main realm being resolved. This change takes the `ContextState` struct added in #17050, and adds to it a `js_recv_cb` callback for each realm. Combined with the fact that that same PR also added a list of known realms to `JsRuntimeState`, and that #17174 made `OpCtx` instances realm-specific and had them include an index into that list of known realms, this makes it possible to know the current realm in the `queue_async_op` and `queue_fast_async_op` methods, and therefore to send the results of promises for each realm to that realm, and prevent the ID's from getting mixed up. Additionally, since promise ID's are no longer unique to the isolate, having a single set of unrefed ops doesn't work. This change therefore also moves `unrefed_ops` from `JsRuntimeState` to `ContextState`, and adds the lengths of the unrefed op sets for all known realms to get the total number of unrefed ops to compare in the event loop. This PR is a reland of #14734 after it was reverted in #16366, except that `ContextState` and `JsRuntimeState::known_realms` were previously relanded in #17050. Another significant difference with the original PR is passing around an index into `JsRuntimeState::known_realms` instead of a `v8::Global<v8::Context>` to identify the realm, because async op queuing in fast calls cannot call into V8, and therefore cannot have access to V8 globals. This also simplified the implementation of `resolve_async_ops`. Co-authored-by: Luis Malheiro <luismalheiro@gmail.com>
2023-01-14 08:40:16 -05:00
let realm_idx = __ctx.realm_idx;
let result = _ops::queue_fast_async_op(__ctx, async move {
let result = result.await;
feat(core): Reland support for async ops in realms (#17204) Currently realms are supported on `deno_core`, but there was no support for async ops anywhere other than the main realm. The main issue is that the `js_recv_cb` callback, which resolves promises corresponding to async ops, was only set for the main realm, so async ops in other realms would never resolve. Furthermore, promise ID's are specific to each realm, which meant that async ops from other realms would result in a wrong promise from the main realm being resolved. This change takes the `ContextState` struct added in #17050, and adds to it a `js_recv_cb` callback for each realm. Combined with the fact that that same PR also added a list of known realms to `JsRuntimeState`, and that #17174 made `OpCtx` instances realm-specific and had them include an index into that list of known realms, this makes it possible to know the current realm in the `queue_async_op` and `queue_fast_async_op` methods, and therefore to send the results of promises for each realm to that realm, and prevent the ID's from getting mixed up. Additionally, since promise ID's are no longer unique to the isolate, having a single set of unrefed ops doesn't work. This change therefore also moves `unrefed_ops` from `JsRuntimeState` to `ContextState`, and adds the lengths of the unrefed op sets for all known realms to get the total number of unrefed ops to compare in the event loop. This PR is a reland of #14734 after it was reverted in #16366, except that `ContextState` and `JsRuntimeState::known_realms` were previously relanded in #17050. Another significant difference with the original PR is passing around an index into `JsRuntimeState::known_realms` instead of a `v8::Global<v8::Context>` to identify the realm, because async op queuing in fast calls cannot call into V8, and therefore cannot have access to V8 globals. This also simplified the implementation of `resolve_async_ops`. Co-authored-by: Luis Malheiro <luismalheiro@gmail.com>
2023-01-14 08:40:16 -05:00
(
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! {
#[inline(always)]
fn function(&self) -> *const ::std::ffi::c_void {
#fast_fn_ident #caller_generics as *const ::std::ffi::c_void
}
},
parse_quote! {
#[inline(always)]
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! {
#[inline(always)]
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,
})
}