diff --git a/.changes/async-commands.md b/.changes/async-commands.md new file mode 100644 index 00000000000..49e7831cf42 --- /dev/null +++ b/.changes/async-commands.md @@ -0,0 +1,6 @@ +--- +"tauri": patch +"tauri-macros": patch +--- + +Only commands with a `async fn` are executed on a separate task. `#[command] fn command_name` runs on the main thread. diff --git a/.changes/command-return.md b/.changes/command-return.md new file mode 100644 index 00000000000..21b918d7451 --- /dev/null +++ b/.changes/command-return.md @@ -0,0 +1,6 @@ +--- +"tauri": patch +"tauri-macros": patch +--- + +Improves support for commands returning `Result`. diff --git a/core/tauri-macros/src/command/mod.rs b/core/tauri-macros/src/command/mod.rs index b8abe499757..9acbb0bda51 100644 --- a/core/tauri-macros/src/command/mod.rs +++ b/core/tauri-macros/src/command/mod.rs @@ -5,10 +5,7 @@ use proc_macro2::Ident; use syn::{Path, PathSegment}; -pub use self::{ - handler::Handler, - wrapper::{Wrapper, WrapperBody}, -}; +pub use self::{handler::Handler, wrapper::wrapper}; mod handler; mod wrapper; diff --git a/core/tauri-macros/src/command/wrapper.rs b/core/tauri-macros/src/command/wrapper.rs index bb8a84e5e7d..b86d302ba9d 100644 --- a/core/tauri-macros/src/command/wrapper.rs +++ b/core/tauri-macros/src/command/wrapper.rs @@ -2,147 +2,147 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use proc_macro2::TokenStream; -use quote::{quote, ToTokens, TokenStreamExt}; -use std::convert::TryFrom; -use syn::{spanned::Spanned, FnArg, Ident, ItemFn, Pat, ReturnType, Type, Visibility}; - -/// The command wrapper created for a function marked with `#[command]`. -pub struct Wrapper { - function: ItemFn, - visibility: Visibility, - maybe_export: TokenStream, - wrapper: Ident, - body: syn::Result, +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse::{Parse, ParseBuffer}, + parse_macro_input, + spanned::Spanned, + FnArg, Ident, ItemFn, Pat, Token, Visibility, +}; + +/// The execution context of the command. +enum ExecutionContext { + Async, + Blocking, } -impl Wrapper { - /// Create a new [`Wrapper`] from the function and the generated code parsed from the function. - pub fn new(function: ItemFn, body: syn::Result) -> Self { - // macros used with `pub use my_macro;` need to be exported with `#[macro_export]` - let maybe_export = match &function.vis { - Visibility::Public(_) => quote!(#[macro_export]), - _ => Default::default(), - }; - - let visibility = function.vis.clone(); - let wrapper = super::format_command_wrapper(&function.sig.ident); - - Self { - function, - visibility, - maybe_export, - wrapper, - body, +impl Parse for ExecutionContext { + fn parse(input: &ParseBuffer) -> syn::Result { + if input.is_empty() { + return Ok(Self::Blocking); } + + input + .parse::() + .map(|_| Self::Async) + .map_err(|_| { + syn::Error::new( + input.span(), + "only a single item `async` is currently allowed", + ) + }) } } -impl From for proc_macro::TokenStream { - fn from( - Wrapper { - function, - maybe_export, - wrapper, - body, - visibility, - }: Wrapper, - ) -> Self { - // either use the successful body or a `compile_error!` of the error occurred while parsing it. - let body = body - .as_ref() - .map(ToTokens::to_token_stream) - .unwrap_or_else(syn::Error::to_compile_error); - - // we `use` the macro so that other modules can resolve the with the same path as the function. - // this is dependent on rust 2018 edition. - quote!( - #function - #maybe_export - macro_rules! #wrapper { ($path:path, $invoke:ident) => {{ #body }}; } - #visibility use #wrapper; - ) - .into() - } +/// Create a new [`Wrapper`] from the function and the generated code parsed from the function. +pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { + let function = parse_macro_input!(item as ItemFn); + let wrapper = super::format_command_wrapper(&function.sig.ident); + let visibility = &function.vis; + + // macros used with `pub use my_macro;` need to be exported with `#[macro_export]` + let maybe_macro_export = match &function.vis { + Visibility::Public(_) => quote!(#[macro_export]), + _ => Default::default(), + }; + + // body to the command wrapper or a `compile_error!` of an error occurred while parsing it. + let body = syn::parse::(attributes) + .map(|context| match function.sig.asyncness { + Some(_) => ExecutionContext::Async, + None => context, + }) + .and_then(|context| match context { + ExecutionContext::Async => body_async(&function), + ExecutionContext::Blocking => body_blocking(&function), + }) + .unwrap_or_else(syn::Error::into_compile_error); + + // Rely on rust 2018 edition to allow importing a macro from a path. + quote!( + #function + + #maybe_macro_export + macro_rules! #wrapper { + // double braces because the item is expected to be a block expression + ($path:path, $invoke:ident) => {{ + // import all the autoref specialization items + #[allow(unused_imports)] + use ::tauri::command::private::*; + + // prevent warnings when the body is a `compile_error!` or if the command has no arguments + #[allow(unused_variables)] + let ::tauri::Invoke { message, resolver } = $invoke; + + #body + }}; + } + + // allow the macro to be resolved with the same path as the command function + #[allow(unused_imports)] + #visibility use #wrapper; + ) + .into() } -/// Body of the wrapper that maps the command parameters into callable arguments from [`Invoke`]. +/// Generates an asynchronous command response from the arguments and return value of a function. /// -/// This is possible because we require the command parameters to be [`CommandArg`] and use type -/// inference to put values generated from that trait into the arguments of the called command. +/// See the [`tauri::command`] module for all the items and traits that make this possible. /// -/// [`CommandArg`]: https://docs.rs/tauri/*/tauri/command/trait.CommandArg.html -/// [`Invoke`]: https://docs.rs/tauri/*/tauri/struct.Invoke.html -pub struct WrapperBody(TokenStream); - -impl TryFrom<&ItemFn> for WrapperBody { - type Error = syn::Error; - - fn try_from(function: &ItemFn) -> syn::Result { - // the name of the #[command] function is the name of the command to handle - let command = function.sig.ident.clone(); - - // automatically append await when the #[command] function is async - let maybe_await = match function.sig.asyncness { - Some(_) => quote!(.await), - None => Default::default(), - }; - - // todo: detect command return types automatically like params, removes parsing type name - let returns_result = match function.sig.output { - ReturnType::Type(_, ref ty) => match &**ty { - Type::Path(type_path) => type_path - .path - .segments - .first() - .map(|seg| seg.ident == "Result") - .unwrap_or_default(), - _ => false, - }, - ReturnType::Default => false, - }; - - let mut args = Vec::new(); - for param in &function.sig.inputs { - args.push(parse_arg(&command, param)?); - } - - // todo: change this to automatically detect result returns (see above result todo) - // if the command handler returns a Result, - // we just map the values to the ones expected by Tauri - // otherwise we wrap it with an `Ok()`, converting the return value to tauri::InvokeResponse - // note that all types must implement `serde::Serialize`. - let result = if returns_result { - quote! { - let result = $path(#(#args?),*); - ::core::result::Result::Ok(result #maybe_await?) - } - } else { - quote! { +/// * Requires binding `message` and `resolver`. +/// * Requires all the traits from `tauri::command::private` to be in scope. +/// +/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html +fn body_async(function: &ItemFn) -> syn::Result { + parse_args(function).map(|args| { + quote! { + resolver.respond_async_serialized(async move { let result = $path(#(#args?),*); - ::core::result::Result::<_, ::tauri::InvokeError>::Ok(result #maybe_await) - } - }; - - Ok(Self(result)) - } + (&result).async_kind().future(result).await + }) + } + }) } -impl ToTokens for WrapperBody { - fn to_tokens(&self, tokens: &mut TokenStream) { - let body = &self.0; +/// Generates a blocking command response from the arguments and return value of a function. +/// +/// See the [`tauri::command`] module for all the items and traits that make this possible. +/// +/// * Requires binding `message` and `resolver`. +/// * Requires all the traits from `tauri::command::private` to be in scope. +/// +/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html +fn body_blocking(function: &ItemFn) -> syn::Result { + let args = parse_args(function)?; + + // the body of a `match` to early return any argument that wasn't successful in parsing. + let match_body = quote!({ + Ok(arg) => arg, + Err(err) => return resolver.invoke_error(err), + }); + + Ok(quote! { + let result = $path(#(match #args #match_body),*); + (&result).blocking_kind().block(result, resolver); + }) +} - // we #[allow(unused_variables)] because a command with no arguments will not use message. - tokens.append_all(quote!( - #[allow(unused_variables)] - let ::tauri::Invoke { message, resolver } = $invoke; - resolver.respond_async(async move { #body }); - )) - } +/// Parse all arguments for the command wrapper to use from the signature of the command function. +fn parse_args(function: &ItemFn) -> syn::Result> { + function + .sig + .inputs + .iter() + .map(|arg| parse_arg(&function.sig.ident, arg)) + .collect() } -/// Transform a [`FnArg`] into a command argument. Expects borrowable binding `message` to exist. -fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result { +/// Transform a [`FnArg`] into a command argument. +/// +/// * Requires binding `message`. +fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result { // we have no use for self arguments let mut arg = match arg { FnArg::Typed(arg) => arg.pat.as_ref().clone(), @@ -154,7 +154,7 @@ fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result { } }; - // we only support patterns supported as arguments to a `ItemFn`. + // we only support patterns that allow us to extract some sort of keyed identifier. let key = match &mut arg { Pat::Ident(arg) => arg.ident.to_string(), Pat::Wild(_) => "_".into(), diff --git a/core/tauri-macros/src/lib.rs b/core/tauri-macros/src/lib.rs index e279f7c80ba..5d57c034f20 100644 --- a/core/tauri-macros/src/lib.rs +++ b/core/tauri-macros/src/lib.rs @@ -5,8 +5,7 @@ extern crate proc_macro; use crate::context::ContextItems; use proc_macro::TokenStream; -use std::convert::TryFrom; -use syn::{parse_macro_input, ItemFn}; +use syn::parse_macro_input; mod command; @@ -14,10 +13,8 @@ mod command; mod context; #[proc_macro_attribute] -pub fn command(_attrs: TokenStream, item: TokenStream) -> TokenStream { - let function = parse_macro_input!(item as ItemFn); - let body = command::WrapperBody::try_from(&function); - command::Wrapper::new(function, body).into() +pub fn command(attributes: TokenStream, item: TokenStream) -> TokenStream { + command::wrapper(attributes, item) } #[proc_macro] diff --git a/core/tauri/src/command.rs b/core/tauri/src/command.rs index 31a053f13a2..52188fa33c4 100644 --- a/core/tauri/src/command.rs +++ b/core/tauri/src/command.rs @@ -7,7 +7,7 @@ use crate::hooks::InvokeError; use crate::{InvokeMessage, Params}; use serde::de::Visitor; -use serde::Deserializer; +use serde::{Deserialize, Deserializer}; /// Represents a custom command. pub struct CommandItem<'a, P: Params> { @@ -44,7 +44,7 @@ pub trait CommandArg<'de, P: Params>: Sized { } /// Automatically implement [`CommandArg`] for any type that can be deserialized. -impl<'de, D: serde::Deserialize<'de>, P: Params> CommandArg<'de, P> for D { +impl<'de, D: Deserialize<'de>, P: Params> CommandArg<'de, P> for D { fn from_command(command: CommandItem<'de, P>) -> Result { let arg = command.key; Self::deserialize(command).map_err(|e| crate::Error::InvalidArgs(arg, e).into()) @@ -137,3 +137,146 @@ impl<'de, P: Params> Deserializer<'de> for CommandItem<'de, P> { pass!(deserialize_identifier, visitor: V); pass!(deserialize_ignored_any, visitor: V); } + +/// [Autoref-based stable specialization](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) +#[doc(hidden)] +pub mod private { + use crate::{InvokeError, InvokeResolver, Params}; + use futures::{FutureExt, TryFutureExt}; + use serde::Serialize; + use serde_json::Value; + use std::future::Future; + + // ===== impl Serialize ===== + + pub struct SerializeTag; + + pub trait SerializeKind { + #[inline(always)] + fn blocking_kind(&self) -> SerializeTag { + SerializeTag + } + + #[inline(always)] + fn async_kind(&self) -> SerializeTag { + SerializeTag + } + } + + impl SerializeKind for &T {} + + impl SerializeTag { + #[inline(always)] + pub fn block(self, value: T, resolver: InvokeResolver

) + where + P: Params, + T: Serialize, + { + resolver.respond(Ok(value)) + } + + #[inline(always)] + pub fn future(self, value: T) -> impl Future> + where + T: Serialize, + { + std::future::ready(serde_json::to_value(value).map_err(InvokeError::from_serde_json)) + } + } + + // ===== Result> ===== + + pub struct ResultTag; + + pub trait ResultKind { + #[inline(always)] + fn blocking_kind(&self) -> ResultTag { + ResultTag + } + + #[inline(always)] + fn async_kind(&self) -> ResultTag { + ResultTag + } + } + + impl> ResultKind for Result {} + + impl ResultTag { + #[inline(always)] + pub fn block(self, value: Result, resolver: InvokeResolver

) + where + P: Params, + T: Serialize, + E: Into, + { + resolver.respond(value.map_err(Into::into)) + } + + #[inline(always)] + pub fn future( + self, + value: Result, + ) -> impl Future> + where + T: Serialize, + E: Into, + { + std::future::ready( + value + .map_err(Into::into) + .and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)), + ) + } + } + + // ===== Future ===== + + pub struct FutureTag; + + pub trait FutureKind { + #[inline(always)] + fn async_kind(&self) -> FutureTag { + FutureTag + } + } + impl> FutureKind for &F {} + + impl FutureTag { + #[inline(always)] + pub fn future(self, value: F) -> impl Future> + where + T: Serialize, + F: Future + Send + 'static, + { + value.map(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)) + } + } + + // ===== Future>> ===== + + pub struct ResultFutureTag; + + pub trait ResultFutureKind { + #[inline(always)] + fn async_kind(&self) -> ResultFutureTag { + ResultFutureTag + } + } + + impl, F: Future>> ResultFutureKind for F {} + + impl ResultFutureTag { + #[inline(always)] + pub fn future(self, value: F) -> impl Future> + where + T: Serialize, + E: Into, + F: Future> + Send, + { + value.err_into().map(|result| { + result.and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)) + }) + } + } +} diff --git a/core/tauri/src/hooks.rs b/core/tauri/src/hooks.rs index c66ab77d620..215af567d1c 100644 --- a/core/tauri/src/hooks.rs +++ b/core/tauri/src/hooks.rs @@ -49,12 +49,14 @@ pub struct InvokeError(JsonValue); impl InvokeError { /// Create an [`InvokeError`] as a string of the [`serde_json::Error`] message. + #[inline(always)] pub fn from_serde_json(error: serde_json::Error) -> Self { Self(JsonValue::String(error.to_string())) } } impl From for InvokeError { + #[inline] fn from(value: T) -> Self { serde_json::to_value(value) .map(Self) @@ -63,6 +65,7 @@ impl From for InvokeError { } impl From for InvokeError { + #[inline(always)] fn from(error: crate::Error) -> Self { Self(JsonValue::String(error.to_string())) } @@ -79,6 +82,7 @@ pub enum InvokeResponse { impl InvokeResponse { /// Turn a [`InvokeResponse`] back into a serializable result. + #[inline(always)] pub fn into_result(self) -> Result { match self { Self::Ok(v) => Ok(v), @@ -88,6 +92,7 @@ impl InvokeResponse { } impl From> for InvokeResponse { + #[inline] fn from(result: Result) -> Self { match result { Ok(ok) => match serde_json::to_value(ok) { @@ -99,9 +104,15 @@ impl From> for InvokeResponse { } } +impl From for InvokeResponse { + fn from(error: InvokeError) -> Self { + Self::Err(error) + } +} + /// Resolver of a invoke message. -pub struct InvokeResolver { - window: Window, +pub struct InvokeResolver { + window: Window

, pub(crate) callback: String, pub(crate) error: String, } @@ -126,6 +137,21 @@ impl InvokeResolver

{ }); } + /// Reply to the invoke promise with an async task which is already serialized. + pub fn respond_async_serialized(self, task: F) + where + F: Future> + Send + 'static, + { + crate::async_runtime::spawn(async move { + Self::return_result(self.window, task.await.into(), self.callback, self.error); + }); + } + + /// Reply to the invoke promise with a serializable value. + pub fn respond(self, value: Result) { + Self::return_result(self.window, value.into(), self.callback, self.error) + } + /// Reply to the invoke promise running the given closure. pub fn respond_closure(self, f: F) where @@ -136,20 +162,25 @@ impl InvokeResolver

{ } /// Resolve the invoke promise with a value. - pub fn resolve(self, value: S) { - Self::return_result(self.window, Ok(value), self.callback, self.error) + pub fn resolve(self, value: T) { + Self::return_result(self.window, Ok(value).into(), self.callback, self.error) } /// Reject the invoke promise with a value. - pub fn reject(self, value: S) { + pub fn reject(self, value: T) { Self::return_result( self.window, - Result::<(), _>::Err(value.into()), + Result::<(), _>::Err(value.into()).into(), self.callback, self.error, ) } + /// Reject the invoke promise with an [`InvokeError`]. + pub fn invoke_error(self, error: InvokeError) { + Self::return_result(self.window, error.into(), self.callback, self.error) + } + /// Asynchronously executes the given task /// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names. /// @@ -174,17 +205,17 @@ impl InvokeResolver

{ success_callback: String, error_callback: String, ) { - Self::return_result(window, f(), success_callback, error_callback) + Self::return_result(window, f().into(), success_callback, error_callback) } - pub(crate) fn return_result( + pub(crate) fn return_result( window: Window

, - response: Result, + response: InvokeResponse, success_callback: String, error_callback: String, ) { let callback_string = match format_callback_result( - InvokeResponse::from(response).into_result(), + response.into_result(), success_callback, error_callback.clone(), ) { diff --git a/examples/commands/public/index.html b/examples/commands/public/index.html index f9a180ff15a..cae0a421f35 100644 --- a/examples/commands/public/index.html +++ b/examples/commands/public/index.html @@ -10,46 +10,48 @@

Tauri Commands

-
Response:
+
Response:
+
Without Args: