Skip to content

Commit

Permalink
perf(core): improve binary size with api enum serde refactor (#3952)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog authored Apr 24, 2022
1 parent f68af45 commit c23f139
Show file tree
Hide file tree
Showing 23 changed files with 388 additions and 197 deletions.
5 changes: 5 additions & 0 deletions .changes/binary-size-perf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri": patch
---

Reduce the amount of generated code for the API endpoints.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ exclude = [

# default to small, optimized workspace release binaries
[profile.release]
strip = true
panic = "abort"
codegen-units = 1
lto = true
Expand Down
203 changes: 150 additions & 53 deletions core/tauri-macros/src/command_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,115 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use heck::ToSnakeCase;
use heck::{ToLowerCamelCase, ToSnakeCase};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
use proc_macro2::{Span, TokenStream as TokenStream2};

use quote::{format_ident, quote, quote_spanned};
use syn::{
parse::{Parse, ParseStream},
parse_quote,
spanned::Spanned,
Data, DeriveInput, Error, Fields, FnArg, Ident, ItemFn, LitStr, Pat, Token,
Data, DeriveInput, Error, Fields, Ident, ItemFn, LitStr, Token,
};

pub fn generate_run_fn(input: DeriveInput) -> TokenStream {
pub(crate) fn generate_command_enum(mut input: DeriveInput) -> TokenStream {
let mut deserialize_functions = TokenStream2::new();
let mut errors = TokenStream2::new();

input.attrs.push(parse_quote!(#[allow(dead_code)]));

match &mut input.data {
Data::Enum(data_enum) => {
for variant in &mut data_enum.variants {
let mut feature: Option<Ident> = None;
let mut error_message: Option<String> = None;

for attr in &variant.attrs {
if attr.path.is_ident("cmd") {
let r = attr
.parse_args_with(|input: ParseStream| {
if let Ok(f) = input.parse::<Ident>() {
feature.replace(f);
input.parse::<Token![,]>()?;
let error_message_raw: LitStr = input.parse()?;
error_message.replace(error_message_raw.value());
}
Ok(quote!())
})
.unwrap_or_else(syn::Error::into_compile_error);
errors.extend(r);
}
}

if let Some(f) = feature {
let error_message = if let Some(e) = error_message {
let e = e.to_string();
quote!(#e)
} else {
quote!("This API is not enabled in the allowlist.")
};

let deserialize_function_name = quote::format_ident!("__{}_deserializer", variant.ident);
deserialize_functions.extend(quote! {
#[cfg(not(#f))]
#[allow(non_snake_case)]
fn #deserialize_function_name<'de, D, T>(deserializer: D) -> ::std::result::Result<T, D::Error>
where
D: ::serde::de::Deserializer<'de>,
{
::std::result::Result::Err(::serde::de::Error::custom(crate::Error::ApiNotAllowlisted(#error_message.into()).to_string()))
}
});

let deserialize_function_name = deserialize_function_name.to_string();

variant
.attrs
.push(parse_quote!(#[cfg_attr(not(#f), serde(deserialize_with = #deserialize_function_name))]));
}
}
}
_ => {
return Error::new(
Span::call_site(),
"`command_enum` is only implemented for enums",
)
.to_compile_error()
.into()
}
};

TokenStream::from(quote! {
#errors
#input
#deserialize_functions
})
}

pub(crate) fn generate_run_fn(input: DeriveInput) -> TokenStream {
let name = &input.ident;
let data = &input.data;

let mut errors = TokenStream2::new();

let mut is_async = false;

let attrs = input.attrs;
for attr in attrs {
if attr.path.is_ident("cmd") {
let _ = attr.parse_args_with(|input: ParseStream| {
while let Some(token) = input.parse()? {
if let TokenTree::Ident(ident) = token {
is_async |= ident == "async";
let r = attr
.parse_args_with(|input: ParseStream| {
if let Ok(token) = input.parse::<Ident>() {
is_async = token == "async";
}
}
Ok(())
});
Ok(quote!())
})
.unwrap_or_else(syn::Error::into_compile_error);
errors.extend(r);
}
}

let maybe_await = if is_async { quote!(.await) } else { quote!() };
let maybe_async = if is_async { quote!(async) } else { quote!() };

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

let mut feature = None;

for attr in &variant.attrs {
if attr.path.is_ident("cmd") {
let r = attr
.parse_args_with(|input: ParseStream| {
if let Ok(f) = input.parse::<Ident>() {
feature.replace(f);
input.parse::<Token![,]>()?;
let _: LitStr = input.parse()?;
}
Ok(quote!())
})
.unwrap_or_else(syn::Error::into_compile_error);
errors.extend(r);
}
}

let maybe_feature_check = if let Some(f) = feature {
quote!(#[cfg(#f)])
} else {
quote!()
};

let (fields_in_variant, variables) = match &variant.fields {
Fields::Unit => (quote_spanned! { variant.span() => }, quote!()),
Fields::Unnamed(fields) => {
Expand Down Expand Up @@ -73,9 +177,13 @@ pub fn generate_run_fn(input: DeriveInput) -> TokenStream {
variant_execute_function_name.set_span(variant_name.span());

matcher.extend(quote_spanned! {
variant.span() => #name::#variant_name #fields_in_variant => #name::#variant_execute_function_name(context, #variables)#maybe_await.map(Into::into),
variant.span() => #maybe_feature_check #name::#variant_name #fields_in_variant => #name::#variant_execute_function_name(context, #variables)#maybe_await.map(Into::into),
});
}

matcher.extend(quote! {
_ => Err(crate::error::into_anyhow("API not in the allowlist (https://tauri.studio/docs/api/config#tauri.allowlist)")),
});
}
_ => {
return Error::new(
Expand All @@ -90,7 +198,8 @@ pub fn generate_run_fn(input: DeriveInput) -> TokenStream {
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

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

impl Parse for HandlerAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let allowlist = input.parse()?;
input.parse::<Token![,]>()?;
let raw: LitStr = input.parse()?;
let error_message = raw.value();
Ok(Self {
allowlist,
error_message,
allowlist: input.parse()?,
})
}
}

pub enum AllowlistCheckKind {
Runtime,
Serde,
}

pub struct HandlerTestAttributes {
allowlist: Ident,
error_message: String,
is_async: bool,
allowlist_check_kind: AllowlistCheckKind,
}

impl Parse for HandlerTestAttributes {
Expand All @@ -133,67 +241,56 @@ impl Parse for HandlerTestAttributes {
input.parse::<Token![,]>()?;
let error_message_raw: LitStr = input.parse()?;
let error_message = error_message_raw.value();
let _ = input.parse::<Token![,]>();
let is_async = input
.parse::<Ident>()
.map(|i| i == "async")
.unwrap_or_default();
let allowlist_check_kind =
if let (Ok(_), Ok(i)) = (input.parse::<Token![,]>(), input.parse::<Ident>()) {
if i == "runtime" {
AllowlistCheckKind::Runtime
} else {
AllowlistCheckKind::Serde
}
} else {
AllowlistCheckKind::Serde
};

Ok(Self {
allowlist,
error_message,
is_async,
allowlist_check_kind,
})
}
}

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

quote!(
#[cfg(#allowlist)]
#function

#[cfg(not(#allowlist))]
#[allow(unused_variables)]
#[allow(unused_mut)]
#signature {
Err(anyhow::anyhow!(crate::Error::ApiNotAllowlisted(#error_message.to_string()).to_string()))
}
)
}

pub fn command_test(attributes: HandlerTestAttributes, function: ItemFn) -> TokenStream2 {
let allowlist = attributes.allowlist;
let is_async = attributes.is_async;
let error_message = attributes.error_message.as_str();
let signature = function.sig.clone();
let test_name = function.sig.ident.clone();
let mut args = quote!();
for arg in &function.sig.inputs {
if let FnArg::Typed(t) = arg {
if let Pat::Ident(i) = &*t.pat {
let ident = &i.ident;
args.extend(quote!(#ident,))
}
}
}

let response = if is_async {
quote!(crate::async_runtime::block_on(
super::Cmd::#test_name(crate::test::mock_invoke_context(), #args)
))
} else {
quote!(super::Cmd::#test_name(crate::test::mock_invoke_context(), #args))
let enum_variant_name = function.sig.ident.to_string().to_lower_camel_case();
let response = match attributes.allowlist_check_kind {
AllowlistCheckKind::Runtime => {
let test_name = function.sig.ident.clone();
quote!(super::Cmd::#test_name(crate::test::mock_invoke_context()))
}
AllowlistCheckKind::Serde => quote! {
serde_json::from_str::<super::Cmd>(&format!(r#"{{ "cmd": "{}", "data": null }}"#, #enum_variant_name))
},
};

quote!(
#[cfg(#allowlist)]
#function

#[cfg(not(#allowlist))]
#[allow(unused_variables)]
#[quickcheck_macros::quickcheck]
#signature {
if let Err(e) = #response {
Expand Down
14 changes: 11 additions & 3 deletions core/tauri-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,25 @@ pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStre
runtime::default_runtime(attributes, input).into()
}

/// Prepares the command module enum.
#[doc(hidden)]
#[proc_macro_derive(CommandModule, attributes(cmd))]
pub fn derive_command_module(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
command_module::generate_run_fn(input)
}

/// Adds a `run` method to an enum (one of the tauri endpoint modules).
/// The `run` method takes a `tauri::endpoints::InvokeContext`
/// and returns a `tauri::Result<tauri::endpoints::InvokeResponse>`.
/// It matches on each enum variant and call a method with name equal to the variant name, lowercased and snake_cased,
/// passing the the context and the variant's fields as arguments.
/// That function must also return the same `Result<InvokeResponse>`.
#[doc(hidden)]
#[proc_macro_derive(CommandModule, attributes(cmd))]
pub fn derive_command_module(input: TokenStream) -> TokenStream {
#[proc_macro_attribute]
pub fn command_enum(_: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
command_module::generate_run_fn(input)
command_module::generate_command_enum(input)
}

#[doc(hidden)]
Expand Down
2 changes: 2 additions & 0 deletions core/tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ fn main() {
shell_execute: { any(shell_all, feature = "shell-execute") },
shell_sidecar: { any(shell_all, feature = "shell-sidecar") },
shell_open: { any(shell_all, feature = "shell-open") },
// helper for the command module macro
shell_script: { any(shell_execute, shell_sidecar) },
// helper for the shell scope functionality
shell_scope: { any(shell_execute, shell_sidecar, feature = "shell-open-api") },

Expand Down
2 changes: 1 addition & 1 deletion core/tauri/scripts/bundle.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion core/tauri/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ mod cli;
mod clipboard;
mod dialog;
mod event;
#[allow(unused_imports)]
mod file_system;
mod global_shortcut;
mod http;
Expand Down
3 changes: 2 additions & 1 deletion core/tauri/src/endpoints/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
use super::InvokeContext;
use crate::Runtime;
use serde::Deserialize;
use tauri_macros::CommandModule;
use tauri_macros::{command_enum, CommandModule};

/// The API descriptor.
#[command_enum]
#[derive(Deserialize, CommandModule)]
#[serde(tag = "cmd", rename_all = "camelCase")]
#[allow(clippy::enum_variant_names)]
Expand Down
Loading

0 comments on commit c23f139

Please sign in to comment.