Skip to content

Commit c23f139

Browse files
authored
perf(core): improve binary size with api enum serde refactor (#3952)
1 parent f68af45 commit c23f139

23 files changed

Lines changed: 388 additions & 197 deletions

.changes/binary-size-perf.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Reduce the amount of generated code for the API endpoints.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ exclude = [
2424

2525
# default to small, optimized workspace release binaries
2626
[profile.release]
27+
strip = true
2728
panic = "abort"
2829
codegen-units = 1
2930
lto = true

core/tauri-macros/src/command_module.rs

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

5-
use heck::ToSnakeCase;
5+
use heck::{ToLowerCamelCase, ToSnakeCase};
66
use proc_macro::TokenStream;
7-
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
7+
use proc_macro2::{Span, TokenStream as TokenStream2};
88

99
use quote::{format_ident, quote, quote_spanned};
1010
use syn::{
1111
parse::{Parse, ParseStream},
12+
parse_quote,
1213
spanned::Spanned,
13-
Data, DeriveInput, Error, Fields, FnArg, Ident, ItemFn, LitStr, Pat, Token,
14+
Data, DeriveInput, Error, Fields, Ident, ItemFn, LitStr, Token,
1415
};
1516

16-
pub fn generate_run_fn(input: DeriveInput) -> TokenStream {
17+
pub(crate) fn generate_command_enum(mut input: DeriveInput) -> TokenStream {
18+
let mut deserialize_functions = TokenStream2::new();
19+
let mut errors = TokenStream2::new();
20+
21+
input.attrs.push(parse_quote!(#[allow(dead_code)]));
22+
23+
match &mut input.data {
24+
Data::Enum(data_enum) => {
25+
for variant in &mut data_enum.variants {
26+
let mut feature: Option<Ident> = None;
27+
let mut error_message: Option<String> = None;
28+
29+
for attr in &variant.attrs {
30+
if attr.path.is_ident("cmd") {
31+
let r = attr
32+
.parse_args_with(|input: ParseStream| {
33+
if let Ok(f) = input.parse::<Ident>() {
34+
feature.replace(f);
35+
input.parse::<Token![,]>()?;
36+
let error_message_raw: LitStr = input.parse()?;
37+
error_message.replace(error_message_raw.value());
38+
}
39+
Ok(quote!())
40+
})
41+
.unwrap_or_else(syn::Error::into_compile_error);
42+
errors.extend(r);
43+
}
44+
}
45+
46+
if let Some(f) = feature {
47+
let error_message = if let Some(e) = error_message {
48+
let e = e.to_string();
49+
quote!(#e)
50+
} else {
51+
quote!("This API is not enabled in the allowlist.")
52+
};
53+
54+
let deserialize_function_name = quote::format_ident!("__{}_deserializer", variant.ident);
55+
deserialize_functions.extend(quote! {
56+
#[cfg(not(#f))]
57+
#[allow(non_snake_case)]
58+
fn #deserialize_function_name<'de, D, T>(deserializer: D) -> ::std::result::Result<T, D::Error>
59+
where
60+
D: ::serde::de::Deserializer<'de>,
61+
{
62+
::std::result::Result::Err(::serde::de::Error::custom(crate::Error::ApiNotAllowlisted(#error_message.into()).to_string()))
63+
}
64+
});
65+
66+
let deserialize_function_name = deserialize_function_name.to_string();
67+
68+
variant
69+
.attrs
70+
.push(parse_quote!(#[cfg_attr(not(#f), serde(deserialize_with = #deserialize_function_name))]));
71+
}
72+
}
73+
}
74+
_ => {
75+
return Error::new(
76+
Span::call_site(),
77+
"`command_enum` is only implemented for enums",
78+
)
79+
.to_compile_error()
80+
.into()
81+
}
82+
};
83+
84+
TokenStream::from(quote! {
85+
#errors
86+
#input
87+
#deserialize_functions
88+
})
89+
}
90+
91+
pub(crate) fn generate_run_fn(input: DeriveInput) -> TokenStream {
1792
let name = &input.ident;
1893
let data = &input.data;
1994

95+
let mut errors = TokenStream2::new();
96+
2097
let mut is_async = false;
98+
2199
let attrs = input.attrs;
22100
for attr in attrs {
23101
if attr.path.is_ident("cmd") {
24-
let _ = attr.parse_args_with(|input: ParseStream| {
25-
while let Some(token) = input.parse()? {
26-
if let TokenTree::Ident(ident) = token {
27-
is_async |= ident == "async";
102+
let r = attr
103+
.parse_args_with(|input: ParseStream| {
104+
if let Ok(token) = input.parse::<Ident>() {
105+
is_async = token == "async";
28106
}
29-
}
30-
Ok(())
31-
});
107+
Ok(quote!())
108+
})
109+
.unwrap_or_else(syn::Error::into_compile_error);
110+
errors.extend(r);
32111
}
33112
}
113+
34114
let maybe_await = if is_async { quote!(.await) } else { quote!() };
35115
let maybe_async = if is_async { quote!(async) } else { quote!() };
36116

@@ -43,6 +123,30 @@ pub fn generate_run_fn(input: DeriveInput) -> TokenStream {
43123
for variant in &data_enum.variants {
44124
let variant_name = &variant.ident;
45125

126+
let mut feature = None;
127+
128+
for attr in &variant.attrs {
129+
if attr.path.is_ident("cmd") {
130+
let r = attr
131+
.parse_args_with(|input: ParseStream| {
132+
if let Ok(f) = input.parse::<Ident>() {
133+
feature.replace(f);
134+
input.parse::<Token![,]>()?;
135+
let _: LitStr = input.parse()?;
136+
}
137+
Ok(quote!())
138+
})
139+
.unwrap_or_else(syn::Error::into_compile_error);
140+
errors.extend(r);
141+
}
142+
}
143+
144+
let maybe_feature_check = if let Some(f) = feature {
145+
quote!(#[cfg(#f)])
146+
} else {
147+
quote!()
148+
};
149+
46150
let (fields_in_variant, variables) = match &variant.fields {
47151
Fields::Unit => (quote_spanned! { variant.span() => }, quote!()),
48152
Fields::Unnamed(fields) => {
@@ -73,9 +177,13 @@ pub fn generate_run_fn(input: DeriveInput) -> TokenStream {
73177
variant_execute_function_name.set_span(variant_name.span());
74178

75179
matcher.extend(quote_spanned! {
76-
variant.span() => #name::#variant_name #fields_in_variant => #name::#variant_execute_function_name(context, #variables)#maybe_await.map(Into::into),
180+
variant.span() => #maybe_feature_check #name::#variant_name #fields_in_variant => #name::#variant_execute_function_name(context, #variables)#maybe_await.map(Into::into),
77181
});
78182
}
183+
184+
matcher.extend(quote! {
185+
_ => Err(crate::error::into_anyhow("API not in the allowlist (https://tauri.studio/docs/api/config#tauri.allowlist)")),
186+
});
79187
}
80188
_ => {
81189
return Error::new(
@@ -90,7 +198,8 @@ pub fn generate_run_fn(input: DeriveInput) -> TokenStream {
90198
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
91199

92200
let expanded = quote! {
93-
impl #impl_generics #name #ty_generics #where_clause {
201+
#errors
202+
impl #impl_generics #name #ty_generics #where_clause {
94203
pub #maybe_async fn run<R: crate::Runtime>(self, context: crate::endpoints::InvokeContext<R>) -> super::Result<crate::endpoints::InvokeResponse> {
95204
match self {
96205
#matcher
@@ -105,26 +214,25 @@ pub fn generate_run_fn(input: DeriveInput) -> TokenStream {
105214
/// Attributes for the module enum variant handler.
106215
pub struct HandlerAttributes {
107216
allowlist: Ident,
108-
error_message: String,
109217
}
110218

111219
impl Parse for HandlerAttributes {
112220
fn parse(input: ParseStream) -> syn::Result<Self> {
113-
let allowlist = input.parse()?;
114-
input.parse::<Token![,]>()?;
115-
let raw: LitStr = input.parse()?;
116-
let error_message = raw.value();
117221
Ok(Self {
118-
allowlist,
119-
error_message,
222+
allowlist: input.parse()?,
120223
})
121224
}
122225
}
123226

227+
pub enum AllowlistCheckKind {
228+
Runtime,
229+
Serde,
230+
}
231+
124232
pub struct HandlerTestAttributes {
125233
allowlist: Ident,
126234
error_message: String,
127-
is_async: bool,
235+
allowlist_check_kind: AllowlistCheckKind,
128236
}
129237

130238
impl Parse for HandlerTestAttributes {
@@ -133,67 +241,56 @@ impl Parse for HandlerTestAttributes {
133241
input.parse::<Token![,]>()?;
134242
let error_message_raw: LitStr = input.parse()?;
135243
let error_message = error_message_raw.value();
136-
let _ = input.parse::<Token![,]>();
137-
let is_async = input
138-
.parse::<Ident>()
139-
.map(|i| i == "async")
140-
.unwrap_or_default();
244+
let allowlist_check_kind =
245+
if let (Ok(_), Ok(i)) = (input.parse::<Token![,]>(), input.parse::<Ident>()) {
246+
if i == "runtime" {
247+
AllowlistCheckKind::Runtime
248+
} else {
249+
AllowlistCheckKind::Serde
250+
}
251+
} else {
252+
AllowlistCheckKind::Serde
253+
};
141254

142255
Ok(Self {
143256
allowlist,
144257
error_message,
145-
is_async,
258+
allowlist_check_kind,
146259
})
147260
}
148261
}
149262

150263
pub fn command_handler(attributes: HandlerAttributes, function: ItemFn) -> TokenStream2 {
151264
let allowlist = attributes.allowlist;
152-
let error_message = attributes.error_message.as_str();
153-
let signature = function.sig.clone();
154265

155266
quote!(
156267
#[cfg(#allowlist)]
157268
#function
158-
159-
#[cfg(not(#allowlist))]
160-
#[allow(unused_variables)]
161-
#[allow(unused_mut)]
162-
#signature {
163-
Err(anyhow::anyhow!(crate::Error::ApiNotAllowlisted(#error_message.to_string()).to_string()))
164-
}
165269
)
166270
}
167271

168272
pub fn command_test(attributes: HandlerTestAttributes, function: ItemFn) -> TokenStream2 {
169273
let allowlist = attributes.allowlist;
170-
let is_async = attributes.is_async;
171274
let error_message = attributes.error_message.as_str();
172275
let signature = function.sig.clone();
173-
let test_name = function.sig.ident.clone();
174-
let mut args = quote!();
175-
for arg in &function.sig.inputs {
176-
if let FnArg::Typed(t) = arg {
177-
if let Pat::Ident(i) = &*t.pat {
178-
let ident = &i.ident;
179-
args.extend(quote!(#ident,))
180-
}
181-
}
182-
}
183276

184-
let response = if is_async {
185-
quote!(crate::async_runtime::block_on(
186-
super::Cmd::#test_name(crate::test::mock_invoke_context(), #args)
187-
))
188-
} else {
189-
quote!(super::Cmd::#test_name(crate::test::mock_invoke_context(), #args))
277+
let enum_variant_name = function.sig.ident.to_string().to_lower_camel_case();
278+
let response = match attributes.allowlist_check_kind {
279+
AllowlistCheckKind::Runtime => {
280+
let test_name = function.sig.ident.clone();
281+
quote!(super::Cmd::#test_name(crate::test::mock_invoke_context()))
282+
}
283+
AllowlistCheckKind::Serde => quote! {
284+
serde_json::from_str::<super::Cmd>(&format!(r#"{{ "cmd": "{}", "data": null }}"#, #enum_variant_name))
285+
},
190286
};
191287

192288
quote!(
193289
#[cfg(#allowlist)]
194290
#function
195291

196292
#[cfg(not(#allowlist))]
293+
#[allow(unused_variables)]
197294
#[quickcheck_macros::quickcheck]
198295
#signature {
199296
if let Err(e) = #response {

core/tauri-macros/src/lib.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,25 @@ pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStre
7676
runtime::default_runtime(attributes, input).into()
7777
}
7878

79+
/// Prepares the command module enum.
80+
#[doc(hidden)]
81+
#[proc_macro_derive(CommandModule, attributes(cmd))]
82+
pub fn derive_command_module(input: TokenStream) -> TokenStream {
83+
let input = parse_macro_input!(input as DeriveInput);
84+
command_module::generate_run_fn(input)
85+
}
86+
7987
/// Adds a `run` method to an enum (one of the tauri endpoint modules).
8088
/// The `run` method takes a `tauri::endpoints::InvokeContext`
8189
/// and returns a `tauri::Result<tauri::endpoints::InvokeResponse>`.
8290
/// It matches on each enum variant and call a method with name equal to the variant name, lowercased and snake_cased,
8391
/// passing the the context and the variant's fields as arguments.
8492
/// That function must also return the same `Result<InvokeResponse>`.
8593
#[doc(hidden)]
86-
#[proc_macro_derive(CommandModule, attributes(cmd))]
87-
pub fn derive_command_module(input: TokenStream) -> TokenStream {
94+
#[proc_macro_attribute]
95+
pub fn command_enum(_: TokenStream, input: TokenStream) -> TokenStream {
8896
let input = parse_macro_input!(input as DeriveInput);
89-
command_module::generate_run_fn(input)
97+
command_module::generate_command_enum(input)
9098
}
9199

92100
#[doc(hidden)]

core/tauri/build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ fn main() {
6060
shell_execute: { any(shell_all, feature = "shell-execute") },
6161
shell_sidecar: { any(shell_all, feature = "shell-sidecar") },
6262
shell_open: { any(shell_all, feature = "shell-open") },
63+
// helper for the command module macro
64+
shell_script: { any(shell_execute, shell_sidecar) },
6365
// helper for the shell scope functionality
6466
shell_scope: { any(shell_execute, shell_sidecar, feature = "shell-open-api") },
6567

core/tauri/scripts/bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/tauri/src/endpoints.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ mod cli;
1717
mod clipboard;
1818
mod dialog;
1919
mod event;
20-
#[allow(unused_imports)]
2120
mod file_system;
2221
mod global_shortcut;
2322
mod http;

core/tauri/src/endpoints/app.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
use super::InvokeContext;
66
use crate::Runtime;
77
use serde::Deserialize;
8-
use tauri_macros::CommandModule;
8+
use tauri_macros::{command_enum, CommandModule};
99

1010
/// The API descriptor.
11+
#[command_enum]
1112
#[derive(Deserialize, CommandModule)]
1213
#[serde(tag = "cmd", rename_all = "camelCase")]
1314
#[allow(clippy::enum_variant_names)]

0 commit comments

Comments
 (0)