// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use core::panic; use once_cell::sync::Lazy; 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::format_ident; use quote::quote; use quote::ToTokens; use regex::Regex; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::FnArg; use syn::GenericParam; use syn::Ident; #[cfg(test)] mod tests; // Identifier 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) } } } #[derive(Copy, Clone, Debug, Default)] struct MacroArgs { is_unstable: bool, is_v8: bool, must_be_fast: bool, } impl syn::parse::Parse for MacroArgs { fn parse(input: syn::parse::ParseStream) -> syn::Result { let vars = syn::punctuated::Punctuated::::parse_terminated( input, )?; let vars: Vec<_> = vars.iter().map(Ident::to_string).collect(); let vars: Vec<_> = vars.iter().map(String::as_str).collect(); for var in vars.iter() { if !["unstable", "v8", "fast"].contains(var) { return Err(syn::Error::new( input.span(), "Ops expect #[op] or #[op(unstable)]", )); } } Ok(Self { is_unstable: vars.contains(&"unstable"), is_v8: vars.contains(&"v8"), must_be_fast: vars.contains(&"fast"), }) } } #[proc_macro_attribute] pub fn op(attr: TokenStream, item: TokenStream) -> TokenStream { let margs = syn::parse_macro_input!(attr as MacroArgs); let MacroArgs { is_unstable, is_v8, must_be_fast, } = margs; let func = syn::parse::(item).expect("expected a function"); let name = &func.sig.ident; let mut generics = func.sig.generics.clone(); let scope_lifetime = syn::LifetimeDef::new(syn::Lifetime::new("'scope", Span::call_site())); if !generics.lifetimes().any(|def| *def == scope_lifetime) { generics .params .push(syn::GenericParam::Lifetime(scope_lifetime)); } let type_params = exclude_lifetime_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 asyncness = func.sig.asyncness.is_some(); let is_async = asyncness || is_future(&func.sig.output); let v8_body = if is_async { codegen_v8_async(&core, &func, margs, asyncness) } else { codegen_v8_sync(&core, &func, margs) }; let (fast_impl, fast_field) = codegen_fast_impl(&core, &func, name, is_async, must_be_fast); let docline = format!("Use `{name}::decl()` to get an op-declaration"); // Generate wrapper quote! { #[allow(non_camel_case_types)] #[doc="Auto-generated by `deno_ops`, i.e: `#[op]`"] #[doc=""] #[doc=#docline] #[doc="you can include in a `deno_core::Extension`."] pub struct #name; #[doc(hidden)] impl #name { pub fn name() -> &'static str { stringify!(#name) } pub fn v8_fn_ptr #generics () -> #core::v8::FunctionCallback #where_clause { use #core::v8::MapFnTo; Self::v8_func::<#type_params>.map_fn_to() } pub fn decl #generics () -> #core::OpDecl #where_clause { #core::OpDecl { name: Self::name(), v8_fn_ptr: Self::v8_fn_ptr::<#type_params>(), enabled: true, fast_fn: #fast_field, is_async: #is_async, is_unstable: #is_unstable, is_v8: #is_v8, } } #[inline] #[allow(clippy::too_many_arguments)] #original_func pub fn v8_func #generics ( scope: &mut #core::v8::HandleScope<'scope>, args: #core::v8::FunctionCallbackArguments, mut rv: #core::v8::ReturnValue, ) #where_clause { #v8_body } } #fast_impl }.into() } /// Generate the body of a v8 func for an async op fn codegen_v8_async( core: &TokenStream2, f: &syn::ItemFn, margs: MacroArgs, asyncness: bool, ) -> TokenStream2 { let MacroArgs { is_v8, .. } = margs; let special_args = f .sig .inputs .iter() .map_while(|a| { (if is_v8 { scope_arg(a) } else { None }).or_else(|| opstate_arg(a)) }) .collect::>(); let rust_i0 = special_args.len(); let args_head = special_args.into_iter().collect::(); let (arg_decls, args_tail) = codegen_args(core, f, rust_i0, 1); let type_params = exclude_lifetime_params(&f.sig.generics.params); let (pre_result, mut result_fut) = match asyncness { true => ( quote! {}, quote! { Self::call::<#type_params>(#args_head #args_tail).await; }, ), false => ( quote! { let result_fut = Self::call::<#type_params>(#args_head #args_tail); }, quote! { result_fut.await; }, ), }; let result_wrapper = match is_result(&f.sig.output) { true => { // Support `Result> + 'static, AnyError>` if !asyncness { result_fut = quote! { result_fut; }; quote! { let result = match result { Ok(fut) => fut.await, Err(e) => return (context, promise_id, op_id, #core::_ops::to_op_result::<()>(get_class, Err(e))), }; } } else { quote! {} } } false => quote! { let result = Ok(result); }, }; quote! { use #core::futures::FutureExt; // SAFETY: #core guarantees args.data() is a v8 External pointing to an OpCtx for the isolates lifetime let ctx = unsafe { &*(#core::v8::Local::<#core::v8::External>::cast(args.data().unwrap_unchecked()).value() as *const #core::_ops::OpCtx) }; let op_id = ctx.id; let promise_id = args.get(0); 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; } }; #arg_decls let state = ctx.state.clone(); // 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 }; let context = { let local = scope.get_current_context(); #core::v8::Global::new(scope, local) }; #pre_result #core::_ops::queue_async_op(scope, async move { let result = #result_fut #result_wrapper (context, promise_id, op_id, #core::_ops::to_op_result(get_class, result)) }); } } fn scope_arg(arg: &FnArg) -> Option { if is_handle_scope(arg) { Some(quote! { scope, }) } else { None } } fn opstate_arg(arg: &FnArg) -> Option { match arg { arg if is_rc_refcell_opstate(arg) => Some(quote! { ctx.state.clone(), }), arg if is_mut_ref_opstate(arg) => { Some(quote! { &mut ctx.state.borrow_mut(), }) } _ => None, } } fn codegen_fast_impl( core: &TokenStream2, f: &syn::ItemFn, name: &syn::Ident, is_async: bool, must_be_fast: bool, ) -> (TokenStream2, TokenStream2) { if !must_be_fast { return (quote! {}, quote! { None }); } let fast_info = can_be_fast_api(core, f); if must_be_fast && fast_info.is_none() { panic!("op cannot be a fast api. enforced by #[op(fast)]") } if must_be_fast && is_async { panic!("async op cannot be a fast api. enforced by #[op(fast)]") } if !is_async { if let Some(FastApiSyn { args, ret, use_recv, }) = fast_info { let inputs = &f .sig .inputs .iter() .skip(if use_recv { 1 } else { 0 }) .collect::>(); let input_idents = f .sig .inputs .iter() .map(|a| match a { FnArg::Receiver(_) => unreachable!(), FnArg::Typed(t) => match &*t.pat { syn::Pat::Ident(i) => format_ident!("{}", i.ident), _ => unreachable!(), }, }) .collect::>(); let generics = &f.sig.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let type_params = exclude_lifetime_params(&f.sig.generics.params); let (trampoline, raw_block) = if is_async { // TODO(@littledivy): Fast async calls. ( quote! { fn func(recv: #core::v8::Local<#core::v8::Object>, __promise_id: u32, #(#inputs),*) { let op_ctx = recv.get_aligned_pointer_from_internal_field(#core::_ops::V8_WRAPPER_OBJECT_INDEX); let op_id = op_ctx.op_id; #core::_ops::queue_async_op(scope, async move { let result = Self::call(#args); (__promise_id, __op_id, #core::_ops::OpResult::Ok(result)) }); } func as *const _ }, quote! {}, ) } else { let output = &f.sig.output; let func_name = format_ident!("func_{}", name); let recv_decl = if use_recv { quote! { let ptr = unsafe { recv.get_aligned_pointer_from_internal_field(#core::_ops::V8_WRAPPER_OBJECT_INDEX) }; let op_ctx = unsafe { &*(ptr as *const #core::_ops::OpCtx) }; let state = &mut op_ctx.state.borrow_mut(); } } else { quote!() }; ( quote! { fn #func_name #generics (recv: #core::v8::Local<#core::v8::Object>, #(#inputs),*) #output #where_clause { #recv_decl #name::call::<#type_params>(#(#input_idents),*) } }, quote! { #func_name #ty_generics as *const _ }, ) }; return ( quote! { #trampoline impl #impl_generics #core::v8::fast_api::FastFunction for #name #ty_generics { fn function(&self) -> *const ::std::ffi::c_void { #raw_block } fn args(&self) -> &'static [#core::v8::fast_api::Type] { &[ #args ] } fn return_type(&self) -> #core::v8::fast_api::CType { #ret } } }, quote! { Some(Box::new(#name #ty_generics)) }, ); } } // Default impl to satisfy generic bounds for non-fast ops (quote! {}, quote! { None }) } /// Generate the body of a v8 func for a sync op fn codegen_v8_sync( core: &TokenStream2, f: &syn::ItemFn, margs: MacroArgs, ) -> TokenStream2 { let MacroArgs { is_v8, .. } = margs; let special_args = f .sig .inputs .iter() .map_while(|a| { (if is_v8 { scope_arg(a) } else { None }).or_else(|| opstate_arg(a)) }) .collect::>(); let rust_i0 = special_args.len(); let args_head = special_args.into_iter().collect::(); let (arg_decls, args_tail) = codegen_args(core, f, rust_i0, 0); let ret = codegen_sync_ret(core, &f.sig.output); let type_params = exclude_lifetime_params(&f.sig.generics.params); quote! { // SAFETY: #core guarantees args.data() is a v8 External pointing to an OpCtx for the isolates lifetime let ctx = unsafe { &*(#core::v8::Local::<#core::v8::External>::cast(args.data().unwrap_unchecked()).value() as *const #core::_ops::OpCtx) }; #arg_decls let result = Self::call::<#type_params>(#args_head #args_tail); let op_state = &*ctx.state.borrow(); op_state.tracker.track_sync(ctx.id); #ret } } struct FastApiSyn { args: TokenStream2, ret: TokenStream2, use_recv: bool, } fn can_be_fast_api(core: &TokenStream2, f: &syn::ItemFn) -> Option { // TODO(@littledivy): Support generics if !f.sig.generics.params.is_empty() { return None; } let inputs = &f.sig.inputs; let ret = match &f.sig.output { syn::ReturnType::Default => quote!(#core::v8::fast_api::CType::Void), syn::ReturnType::Type(_, ty) => match is_fast_scalar(core, ty, true) { Some(ret) => ret, None => return None, }, }; let mut use_recv = false; let mut args = vec![quote! { #core::v8::fast_api::Type::V8Value }]; for (pos, input) in inputs.iter().enumerate() { if pos == 0 && is_mut_ref_opstate(input) { use_recv = true; continue; } let ty = match input { syn::FnArg::Typed(pat) => &pat.ty, _ => unreachable!(), }; match is_fast_scalar(core, ty, false) { None => match is_fast_arg_sequence(core, ty) { Some(arg) => { args.push(arg); } // early return, this function cannot be a fast call. None => return None, }, Some(arg) => { args.push(arg); } } } let args = args .iter() .map(|arg| format!("{}", arg)) .collect::>() .join(", "); Some(FastApiSyn { args: args.parse().unwrap(), ret, use_recv, }) } // A v8::Local or FastApiTypedArray fn is_fast_arg_sequence( core: &TokenStream2, ty: impl ToTokens, ) -> Option { // TODO(@littledivy): Make `v8::` parts optional. if is_fast_typed_array(&ty) { return Some( quote! { #core::v8::fast_api::Type::TypedArray(#core::v8::fast_api::CType::Uint32) }, ); } if is_local_array(&ty) { return Some( quote! { #core::v8::fast_api::Type::Sequence(#core::v8::fast_api::CType::Void) }, ); } None } fn is_local_array(arg: impl ToTokens) -> bool { static RE: Lazy = Lazy::new(|| Regex::new(r"^v8::Local$").unwrap()); RE.is_match(&tokens(arg)) } fn is_fast_typed_array(arg: impl ToTokens) -> bool { static RE: Lazy = Lazy::new(|| { Regex::new(r#": (?:deno_core :: )?FastApiTypedArray$"#).unwrap() }); RE.is_match(&tokens(arg)) } fn is_fast_scalar( core: &TokenStream2, ty: impl ToTokens, is_ret: bool, ) -> Option { let cty = if is_ret { quote! { CType } } else { quote! { Type } }; if is_resource_id(&ty) { return Some(quote! { #core::v8::fast_api::#cty::Uint32 }); } if is_void(&ty) { return Some(quote! { #core::v8::fast_api::#cty::Void }); } // TODO(@littledivy): Support u8, i8, u16, i16 by casting. match tokens(&ty).as_str() { "u32" => Some(quote! { #core::v8::fast_api::#cty::Uint32 }), "i32" => Some(quote! { #core::v8::fast_api::#cty::Int32 }), "f32" => Some(quote! { #core::v8::fast_api::#cty::Float32 }), "f64" => Some(quote! { #core::v8::fast_api::#cty::Float64 }), _ => None, } } fn codegen_args( core: &TokenStream2, f: &syn::ItemFn, rust_i0: usize, // Index of first generic arg in rust v8_i0: usize, // Index of first generic arg in v8/js ) -> (TokenStream2, TokenStream2) { let inputs = &f.sig.inputs.iter().skip(rust_i0).enumerate(); let ident_seq: TokenStream2 = inputs .clone() .map(|(i, _)| format!("arg_{i}")) .collect::>() .join(", ") .parse() .unwrap(); let decls: TokenStream2 = inputs .clone() .map(|(i, arg)| { codegen_arg(core, arg, format!("arg_{i}").as_ref(), v8_i0 + i) }) .collect(); (decls, ident_seq) } fn codegen_arg( core: &TokenStream2, arg: &syn::FnArg, name: &str, idx: usize, ) -> 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 as i32); let #ident = match #core::serde_v8::from_v8(scope, #ident) { Ok(v) => v, Err(err) => { let msg = format!("Error parsing args at position {}: {}", #idx, #core::anyhow::Error::from(err)); return #core::_ops::throw_type_error(scope, msg); } }; } } fn codegen_sync_ret( core: &TokenStream2, output: &syn::ReturnType, ) -> TokenStream2 { if is_void(output) { return quote! {}; } if is_u32_rv(output) { return quote! { rv.set_uint32(result as u32); }; } // Optimize Result<(), Err> to skip serde_v8 when Ok(...) let ok_block = if is_unit_result(output) { quote! {} } else if is_u32_rv_result(output) { quote! { rv.set_uint32(result as u32); } } else { quote! { match #core::serde_v8::to_v8(scope, result) { Ok(ret) => rv.set(ret), Err(err) => #core::_ops::throw_type_error( scope, format!("Error serializing return: {}", #core::anyhow::Error::from(err)), ), }; } }; if !is_result(output) { return ok_block; } quote! { match result { Ok(result) => { #ok_block }, Err(err) => { let exception = #core::error::to_v8_error(scope, op_state.get_error_class_fn, &err); scope.throw_exception(exception); }, }; } } fn is_void(ty: impl ToTokens) -> bool { tokens(ty).is_empty() } fn is_result(ty: impl ToTokens) -> bool { let tokens = tokens(ty); if tokens.trim_start_matches("-> ").starts_with("Result <") { return true; } // Detect `io::Result<...>`, `anyhow::Result<...>`, etc... // i.e: Result aliases/shorthands which are unfortunately "opaque" at macro-time match tokens.find(":: Result <") { Some(idx) => !tokens.split_at(idx).0.contains('<'), None => false, } } /// Detects if the type can be set using `rv.set_uint32` fast path fn is_u32_rv(ty: impl ToTokens) -> bool { ["u32", "u8", "u16"].iter().any(|&s| tokens(&ty) == s) || is_resource_id(&ty) } /// Detects if the type is of the format Result fn is_u32_rv_result(ty: impl ToTokens) -> bool { is_result(&ty) && (tokens(&ty).contains("Result < u32") || tokens(&ty).contains("Result < u8") || tokens(&ty).contains("Result < u16") || is_resource_id(&ty)) } /// Detects if a type is of the form Result<(), Err> fn is_unit_result(ty: impl ToTokens) -> bool { is_result(&ty) && tokens(&ty).contains("Result < ()") } fn is_resource_id(arg: impl ToTokens) -> bool { static RE: Lazy = Lazy::new(|| Regex::new(r#": (?:deno_core :: )?ResourceId$"#).unwrap()); RE.is_match(&tokens(arg)) } fn is_mut_ref_opstate(arg: impl ToTokens) -> bool { static RE: Lazy = Lazy::new(|| Regex::new(r#": & mut (?:deno_core :: )?OpState$"#).unwrap()); RE.is_match(&tokens(arg)) } fn is_rc_refcell_opstate(arg: &syn::FnArg) -> bool { static RE: Lazy = Lazy::new(|| { Regex::new(r#": Rc < RefCell < (?:deno_core :: )?OpState > >$"#).unwrap() }); RE.is_match(&tokens(arg)) } fn is_handle_scope(arg: &syn::FnArg) -> bool { static RE: Lazy = Lazy::new(|| { Regex::new(r#": & mut (?:deno_core :: )?v8 :: HandleScope(?: < '\w+ >)?$"#) .unwrap() }); RE.is_match(&tokens(arg)) } fn is_future(ty: impl ToTokens) -> bool { tokens(&ty).contains("impl Future < Output =") } fn tokens(x: impl ToTokens) -> String { x.to_token_stream().to_string() } fn exclude_lifetime_params( generic_params: &Punctuated, ) -> Punctuated { generic_params .iter() .filter(|t| !tokens(t).starts_with('\'')) .cloned() .collect::>() }