diff options
author | Divy Srivastava <dj.srivastava23@gmail.com> | 2022-03-14 23:14:15 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-14 18:44:15 +0100 |
commit | b4e42953e1d243f2eda20e5be6b845d60b7bf688 (patch) | |
tree | 10b3bfff165f9c04f9174c7c399d44b9b724c3b3 /ops/lib.rs | |
parent | 4e3ed37037a2aa1edeac260dc3463a81d9cf9b88 (diff) |
feat(core): codegen ops (#13861)
Co-authored-by: Aaron O'Mullan <aaron.omullan@gmail.com>
Diffstat (limited to 'ops/lib.rs')
-rw-r--r-- | ops/lib.rs | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/ops/lib.rs b/ops/lib.rs new file mode 100644 index 000000000..d159c6c4b --- /dev/null +++ b/ops/lib.rs @@ -0,0 +1,264 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +use proc_macro::TokenStream; +use proc_macro2::Span; +use proc_macro2::TokenStream as TokenStream2; +use proc_macro_crate::crate_name; +use proc_macro_crate::FoundCrate; +use quote::quote; +use syn::Ident; + +// Identifer to the `deno_core` crate. +// +// If macro called in deno_core, `crate` is used. +// If macro called outside deno_core, `deno_core` OR the renamed +// version from Cargo.toml is used. +fn core_import() -> TokenStream2 { + let found_crate = + crate_name("deno_core").expect("deno_core not present in `Cargo.toml`"); + + match found_crate { + FoundCrate::Itself => { + // TODO(@littledivy): This won't work for `deno_core` examples + // since `crate` does not refer to `deno_core`. + // examples must re-export deno_core to make this work + // until Span inspection APIs are stabalized. + // + // https://github.com/rust-lang/rust/issues/54725 + quote!(crate) + } + FoundCrate::Name(name) => { + let ident = Ident::new(&name, Span::call_site()); + quote!(#ident) + } + } +} + +#[proc_macro_attribute] +pub fn op(_attr: TokenStream, item: TokenStream) -> TokenStream { + let func = syn::parse::<syn::ItemFn>(item).expect("expected a function"); + let name = &func.sig.ident; + let generics = &func.sig.generics; + let type_params = &func.sig.generics.params; + let where_clause = &func.sig.generics.where_clause; + + // Preserve the original func as op_foo::call() + let original_func = { + let mut func = func.clone(); + func.sig.ident = quote::format_ident!("call"); + func + }; + + let core = core_import(); + + let v8_body = if func.sig.asyncness.is_some() { + codegen_v8_async(&core, &func) + } else { + codegen_v8_sync(&core, &func) + }; + + // Generate wrapper + quote! { + #[allow(non_camel_case_types)] + pub struct #name; + + impl #name { + pub fn name() -> &'static str { + stringify!(#name) + } + + pub fn v8_cb #generics () -> #core::v8::FunctionCallback #where_clause { + use #core::v8::MapFnTo; + Self::v8_func::<#type_params>.map_fn_to() + } + + pub fn decl #generics () -> (&'static str, #core::v8::FunctionCallback) #where_clause { + (Self::name(), Self::v8_cb::<#type_params>()) + } + + #[inline] + #original_func + + pub fn v8_func #generics ( + scope: &mut #core::v8::HandleScope, + args: #core::v8::FunctionCallbackArguments, + mut rv: #core::v8::ReturnValue, + ) #where_clause { + #v8_body + } + } + }.into() +} + +/// Generate the body of a v8 func for an async op +fn codegen_v8_async(core: &TokenStream2, f: &syn::ItemFn) -> TokenStream2 { + let a = codegen_arg(core, &f.sig.inputs[1], "a", 2); + let b = codegen_arg(core, &f.sig.inputs[2], "b", 3); + let type_params = &f.sig.generics.params; + + quote! { + use #core::futures::FutureExt; + // SAFETY: Called from Deno.core.opAsync. Which retrieves the index using opId table. + let op_id = unsafe { + #core::v8::Local::<#core::v8::Integer>::cast(args.get(0)) + }.value() as usize; + + let promise_id = args.get(1); + let promise_id = #core::v8::Local::<#core::v8::Integer>::try_from(promise_id) + .map(|l| l.value() as #core::PromiseId) + .map_err(#core::anyhow::Error::from); + // Fail if promise id invalid (not an int) + let promise_id: #core::PromiseId = match promise_id { + Ok(promise_id) => promise_id, + Err(err) => { + #core::_ops::throw_type_error(scope, format!("invalid promise id: {}", err)); + return; + } + }; + + #a + #b + + // SAFETY: Unchecked cast to external since #core guarantees args.data() is a v8 External. + let state_refcell_raw = unsafe { + #core::v8::Local::<#core::v8::External>::cast(args.data().unwrap_unchecked()) + }.value(); + + // SAFETY: The Rc<RefCell<OpState>> is functionally pinned and is tied to the isolate's lifetime + let state = unsafe { + let ptr = state_refcell_raw as *const std::cell::RefCell<#core::OpState>; + // Increment so it will later be decremented/dropped by the underlaying func it is moved to + std::rc::Rc::increment_strong_count(ptr); + std::rc::Rc::from_raw(ptr) + }; + // Track async call & get copy of get_error_class_fn + let get_class = { + let state = state.borrow(); + state.tracker.track_async(op_id); + state.get_error_class_fn + }; + + #core::_ops::queue_async_op(scope, async move { + let result = Self::call::<#type_params>(state, a, b).await; + (promise_id, op_id, #core::_ops::to_op_result(get_class, result)) + }); + } +} + +/// Generate the body of a v8 func for a sync op +fn codegen_v8_sync(core: &TokenStream2, f: &syn::ItemFn) -> TokenStream2 { + let a = codegen_arg(core, &f.sig.inputs[1], "a", 1); + let b = codegen_arg(core, &f.sig.inputs[2], "b", 2); + let ret = codegen_sync_ret(core, &f.sig.output); + let type_params = &f.sig.generics.params; + + quote! { + // SAFETY: Called from Deno.core.opSync. Which retrieves the index using opId table. + let op_id = unsafe { + #core::v8::Local::<#core::v8::Integer>::cast(args.get(0)).value() + } as usize; + + #a + #b + + // SAFETY: Unchecked cast to external since #core guarantees args.data() is a v8 External. + let state_refcell_raw = unsafe { + #core::v8::Local::<#core::v8::External>::cast(args.data().unwrap_unchecked()) + }.value(); + + // SAFETY: The Rc<RefCell<OpState>> is functionally pinned and is tied to the isolate's lifetime + let state = unsafe { &*(state_refcell_raw as *const std::cell::RefCell<#core::OpState>) }; + + let op_state = &mut state.borrow_mut(); + let result = Self::call::<#type_params>(op_state, a, b); + + op_state.tracker.track_sync(op_id); + + #ret + } +} + +fn codegen_arg( + core: &TokenStream2, + arg: &syn::FnArg, + name: &str, + idx: i32, +) -> TokenStream2 { + let ident = quote::format_ident!("{name}"); + let pat = match arg { + syn::FnArg::Typed(pat) => &pat.pat, + _ => unreachable!(), + }; + // Fast path if arg should be skipped + if matches!(**pat, syn::Pat::Wild(_)) { + return quote! { let #ident = (); }; + } + // Otherwise deserialize it via serde_v8 + quote! { + let #ident = args.get(#idx); + let #ident = match #core::serde_v8::from_v8(scope, #ident) { + Ok(v) => v, + Err(err) => { + let msg = format!("Error parsing args: {}", #core::anyhow::Error::from(err)); + return #core::_ops::throw_type_error(scope, msg); + } + }; + } +} + +fn codegen_sync_ret( + core: &TokenStream2, + output: &syn::ReturnType, +) -> TokenStream2 { + let ret_type = match output { + // Func with no return no-ops + syn::ReturnType::Default => return quote! { let ret = (); }, + // Func with a return Result<T, E> + syn::ReturnType::Type(_, ty) => ty, + }; + + // Optimize Result<(), Err> to skip serde_v8 when Ok(...) + let ok_block = if is_unit_result(&**ret_type) { + quote! {} + } else { + quote! { + let ret = #core::serde_v8::to_v8(scope, v).unwrap(); + rv.set(ret); + } + }; + + quote! { + match result { + Ok(v) => { + #ok_block + }, + Err(err) => { + let err = #core::OpError::new(op_state.get_error_class_fn, err); + rv.set(#core::serde_v8::to_v8(scope, err).unwrap()); + }, + }; + } +} + +/// Detects if a type is of the form Result<(), Err> +fn is_unit_result(ty: &syn::Type) -> bool { + let path = match ty { + syn::Type::Path(ref path) => path, + _ => return false, + }; + + let maybe_result = path.path.segments.first().expect("Invalid return type."); + if maybe_result.ident != "Result" { + return false; + } + assert!(!maybe_result.arguments.is_empty()); + + let args = match &maybe_result.arguments { + syn::PathArguments::AngleBracketed(args) => args, + _ => unreachable!(), + }; + + match args.args.first().unwrap() { + syn::GenericArgument::Type(syn::Type::Tuple(ty)) => ty.elems.is_empty(), + _ => false, + } +} |