Skip to content

Commit bb8dafb

Browse files
feat(core): #[command] return with autoref specialization workaround fix #1672 (#1734)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
1 parent c090927 commit bb8dafb

File tree

10 files changed

+429
-184
lines changed

10 files changed

+429
-184
lines changed

.changes/async-commands.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri": patch
3+
"tauri-macros": patch
4+
---
5+
6+
Only commands with a `async fn` are executed on a separate task. `#[command] fn command_name` runs on the main thread.

.changes/command-return.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri": patch
3+
"tauri-macros": patch
4+
---
5+
6+
Improves support for commands returning `Result`.

core/tauri-macros/src/command/mod.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
use proc_macro2::Ident;
66
use syn::{Path, PathSegment};
77

8-
pub use self::{
9-
handler::Handler,
10-
wrapper::{Wrapper, WrapperBody},
11-
};
8+
pub use self::{handler::Handler, wrapper::wrapper};
129

1310
mod handler;
1411
mod wrapper;

core/tauri-macros/src/command/wrapper.rs

Lines changed: 126 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -2,147 +2,147 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
44

5-
use proc_macro2::TokenStream;
6-
use quote::{quote, ToTokens, TokenStreamExt};
7-
use std::convert::TryFrom;
8-
use syn::{spanned::Spanned, FnArg, Ident, ItemFn, Pat, ReturnType, Type, Visibility};
9-
10-
/// The command wrapper created for a function marked with `#[command]`.
11-
pub struct Wrapper {
12-
function: ItemFn,
13-
visibility: Visibility,
14-
maybe_export: TokenStream,
15-
wrapper: Ident,
16-
body: syn::Result<WrapperBody>,
5+
use proc_macro::TokenStream;
6+
use proc_macro2::TokenStream as TokenStream2;
7+
use quote::quote;
8+
use syn::{
9+
parse::{Parse, ParseBuffer},
10+
parse_macro_input,
11+
spanned::Spanned,
12+
FnArg, Ident, ItemFn, Pat, Token, Visibility,
13+
};
14+
15+
/// The execution context of the command.
16+
enum ExecutionContext {
17+
Async,
18+
Blocking,
1719
}
1820

19-
impl Wrapper {
20-
/// Create a new [`Wrapper`] from the function and the generated code parsed from the function.
21-
pub fn new(function: ItemFn, body: syn::Result<WrapperBody>) -> Self {
22-
// macros used with `pub use my_macro;` need to be exported with `#[macro_export]`
23-
let maybe_export = match &function.vis {
24-
Visibility::Public(_) => quote!(#[macro_export]),
25-
_ => Default::default(),
26-
};
27-
28-
let visibility = function.vis.clone();
29-
let wrapper = super::format_command_wrapper(&function.sig.ident);
30-
31-
Self {
32-
function,
33-
visibility,
34-
maybe_export,
35-
wrapper,
36-
body,
21+
impl Parse for ExecutionContext {
22+
fn parse(input: &ParseBuffer) -> syn::Result<Self> {
23+
if input.is_empty() {
24+
return Ok(Self::Blocking);
3725
}
26+
27+
input
28+
.parse::<Token![async]>()
29+
.map(|_| Self::Async)
30+
.map_err(|_| {
31+
syn::Error::new(
32+
input.span(),
33+
"only a single item `async` is currently allowed",
34+
)
35+
})
3836
}
3937
}
4038

41-
impl From<Wrapper> for proc_macro::TokenStream {
42-
fn from(
43-
Wrapper {
44-
function,
45-
maybe_export,
46-
wrapper,
47-
body,
48-
visibility,
49-
}: Wrapper,
50-
) -> Self {
51-
// either use the successful body or a `compile_error!` of the error occurred while parsing it.
52-
let body = body
53-
.as_ref()
54-
.map(ToTokens::to_token_stream)
55-
.unwrap_or_else(syn::Error::to_compile_error);
56-
57-
// we `use` the macro so that other modules can resolve the with the same path as the function.
58-
// this is dependent on rust 2018 edition.
59-
quote!(
60-
#function
61-
#maybe_export
62-
macro_rules! #wrapper { ($path:path, $invoke:ident) => {{ #body }}; }
63-
#visibility use #wrapper;
64-
)
65-
.into()
66-
}
39+
/// Create a new [`Wrapper`] from the function and the generated code parsed from the function.
40+
pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
41+
let function = parse_macro_input!(item as ItemFn);
42+
let wrapper = super::format_command_wrapper(&function.sig.ident);
43+
let visibility = &function.vis;
44+
45+
// macros used with `pub use my_macro;` need to be exported with `#[macro_export]`
46+
let maybe_macro_export = match &function.vis {
47+
Visibility::Public(_) => quote!(#[macro_export]),
48+
_ => Default::default(),
49+
};
50+
51+
// body to the command wrapper or a `compile_error!` of an error occurred while parsing it.
52+
let body = syn::parse::<ExecutionContext>(attributes)
53+
.map(|context| match function.sig.asyncness {
54+
Some(_) => ExecutionContext::Async,
55+
None => context,
56+
})
57+
.and_then(|context| match context {
58+
ExecutionContext::Async => body_async(&function),
59+
ExecutionContext::Blocking => body_blocking(&function),
60+
})
61+
.unwrap_or_else(syn::Error::into_compile_error);
62+
63+
// Rely on rust 2018 edition to allow importing a macro from a path.
64+
quote!(
65+
#function
66+
67+
#maybe_macro_export
68+
macro_rules! #wrapper {
69+
// double braces because the item is expected to be a block expression
70+
($path:path, $invoke:ident) => {{
71+
// import all the autoref specialization items
72+
#[allow(unused_imports)]
73+
use ::tauri::command::private::*;
74+
75+
// prevent warnings when the body is a `compile_error!` or if the command has no arguments
76+
#[allow(unused_variables)]
77+
let ::tauri::Invoke { message, resolver } = $invoke;
78+
79+
#body
80+
}};
81+
}
82+
83+
// allow the macro to be resolved with the same path as the command function
84+
#[allow(unused_imports)]
85+
#visibility use #wrapper;
86+
)
87+
.into()
6788
}
6889

69-
/// Body of the wrapper that maps the command parameters into callable arguments from [`Invoke`].
90+
/// Generates an asynchronous command response from the arguments and return value of a function.
7091
///
71-
/// This is possible because we require the command parameters to be [`CommandArg`] and use type
72-
/// inference to put values generated from that trait into the arguments of the called command.
92+
/// See the [`tauri::command`] module for all the items and traits that make this possible.
7393
///
74-
/// [`CommandArg`]: https://docs.rs/tauri/*/tauri/command/trait.CommandArg.html
75-
/// [`Invoke`]: https://docs.rs/tauri/*/tauri/struct.Invoke.html
76-
pub struct WrapperBody(TokenStream);
77-
78-
impl TryFrom<&ItemFn> for WrapperBody {
79-
type Error = syn::Error;
80-
81-
fn try_from(function: &ItemFn) -> syn::Result<Self> {
82-
// the name of the #[command] function is the name of the command to handle
83-
let command = function.sig.ident.clone();
84-
85-
// automatically append await when the #[command] function is async
86-
let maybe_await = match function.sig.asyncness {
87-
Some(_) => quote!(.await),
88-
None => Default::default(),
89-
};
90-
91-
// todo: detect command return types automatically like params, removes parsing type name
92-
let returns_result = match function.sig.output {
93-
ReturnType::Type(_, ref ty) => match &**ty {
94-
Type::Path(type_path) => type_path
95-
.path
96-
.segments
97-
.first()
98-
.map(|seg| seg.ident == "Result")
99-
.unwrap_or_default(),
100-
_ => false,
101-
},
102-
ReturnType::Default => false,
103-
};
104-
105-
let mut args = Vec::new();
106-
for param in &function.sig.inputs {
107-
args.push(parse_arg(&command, param)?);
108-
}
109-
110-
// todo: change this to automatically detect result returns (see above result todo)
111-
// if the command handler returns a Result,
112-
// we just map the values to the ones expected by Tauri
113-
// otherwise we wrap it with an `Ok()`, converting the return value to tauri::InvokeResponse
114-
// note that all types must implement `serde::Serialize`.
115-
let result = if returns_result {
116-
quote! {
117-
let result = $path(#(#args?),*);
118-
::core::result::Result::Ok(result #maybe_await?)
119-
}
120-
} else {
121-
quote! {
94+
/// * Requires binding `message` and `resolver`.
95+
/// * Requires all the traits from `tauri::command::private` to be in scope.
96+
///
97+
/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
98+
fn body_async(function: &ItemFn) -> syn::Result<TokenStream2> {
99+
parse_args(function).map(|args| {
100+
quote! {
101+
resolver.respond_async_serialized(async move {
122102
let result = $path(#(#args?),*);
123-
::core::result::Result::<_, ::tauri::InvokeError>::Ok(result #maybe_await)
124-
}
125-
};
126-
127-
Ok(Self(result))
128-
}
103+
(&result).async_kind().future(result).await
104+
})
105+
}
106+
})
129107
}
130108

131-
impl ToTokens for WrapperBody {
132-
fn to_tokens(&self, tokens: &mut TokenStream) {
133-
let body = &self.0;
109+
/// Generates a blocking command response from the arguments and return value of a function.
110+
///
111+
/// See the [`tauri::command`] module for all the items and traits that make this possible.
112+
///
113+
/// * Requires binding `message` and `resolver`.
114+
/// * Requires all the traits from `tauri::command::private` to be in scope.
115+
///
116+
/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
117+
fn body_blocking(function: &ItemFn) -> syn::Result<TokenStream2> {
118+
let args = parse_args(function)?;
119+
120+
// the body of a `match` to early return any argument that wasn't successful in parsing.
121+
let match_body = quote!({
122+
Ok(arg) => arg,
123+
Err(err) => return resolver.invoke_error(err),
124+
});
125+
126+
Ok(quote! {
127+
let result = $path(#(match #args #match_body),*);
128+
(&result).blocking_kind().block(result, resolver);
129+
})
130+
}
134131

135-
// we #[allow(unused_variables)] because a command with no arguments will not use message.
136-
tokens.append_all(quote!(
137-
#[allow(unused_variables)]
138-
let ::tauri::Invoke { message, resolver } = $invoke;
139-
resolver.respond_async(async move { #body });
140-
))
141-
}
132+
/// Parse all arguments for the command wrapper to use from the signature of the command function.
133+
fn parse_args(function: &ItemFn) -> syn::Result<Vec<TokenStream2>> {
134+
function
135+
.sig
136+
.inputs
137+
.iter()
138+
.map(|arg| parse_arg(&function.sig.ident, arg))
139+
.collect()
142140
}
143141

144-
/// Transform a [`FnArg`] into a command argument. Expects borrowable binding `message` to exist.
145-
fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result<TokenStream> {
142+
/// Transform a [`FnArg`] into a command argument.
143+
///
144+
/// * Requires binding `message`.
145+
fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result<TokenStream2> {
146146
// we have no use for self arguments
147147
let mut arg = match arg {
148148
FnArg::Typed(arg) => arg.pat.as_ref().clone(),
@@ -154,7 +154,7 @@ fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result<TokenStream> {
154154
}
155155
};
156156

157-
// we only support patterns supported as arguments to a `ItemFn`.
157+
// we only support patterns that allow us to extract some sort of keyed identifier.
158158
let key = match &mut arg {
159159
Pat::Ident(arg) => arg.ident.to_string(),
160160
Pat::Wild(_) => "_".into(),

core/tauri-macros/src/lib.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,16 @@
55
extern crate proc_macro;
66
use crate::context::ContextItems;
77
use proc_macro::TokenStream;
8-
use std::convert::TryFrom;
9-
use syn::{parse_macro_input, ItemFn};
8+
use syn::parse_macro_input;
109

1110
mod command;
1211

1312
#[macro_use]
1413
mod context;
1514

1615
#[proc_macro_attribute]
17-
pub fn command(_attrs: TokenStream, item: TokenStream) -> TokenStream {
18-
let function = parse_macro_input!(item as ItemFn);
19-
let body = command::WrapperBody::try_from(&function);
20-
command::Wrapper::new(function, body).into()
16+
pub fn command(attributes: TokenStream, item: TokenStream) -> TokenStream {
17+
command::wrapper(attributes, item)
2118
}
2219

2320
#[proc_macro]

0 commit comments

Comments
 (0)