Skip to content

Commit d0510f5

Browse files
authored
refactor(core): serialize response once, closes #5641 (#10641)
* refactor(core): serialize response once closes #5641 This change impacts both the custom protocol and the postMessage based IPC implementations. Basically it changes the whole IPC mechanism to work on raw JSON strings so we do not need to serialize a serde_json::Value after serializing to it from a user-provided type. i benchmarked this with a 150MB file response (returning Vec<u8> instead of tauri::ipc::Response since the latter does not serialize at all) and it went from 29s to 23s (custom protocol) and from 54s to 48s (post message) on macOS. * fix mobile & lint * clippy
1 parent d1ee3f4 commit d0510f5

File tree

17 files changed

+343
-136
lines changed

17 files changed

+343
-136
lines changed

.changes/refactor-ipc-response.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri": patch:breaking
3+
---
4+
5+
Added a dedicated type for IPC response body `InvokeResponseBody` for performance reasons.
6+
This is only a breaking change if you are directly using types from `tauri::ipc`.

core/tauri-config-schema/schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
33
"title": "Config",
4-
"description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n ```json title=\"Example tauri.config.json file\"\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```",
4+
"description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n Example tauri.config.json file:\n\n ```json\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```",
55
"type": "object",
66
"properties": {
77
"$schema": {

core/tauri-runtime-wry/src/lib.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ use wry::WebViewBuilderExtWindows;
4646
use tao::{
4747
dpi::{
4848
LogicalPosition as TaoLogicalPosition, LogicalSize as TaoLogicalSize,
49-
LogicalUnit as ToaLogicalUnit, PhysicalPosition as TaoPhysicalPosition,
50-
PhysicalSize as TaoPhysicalSize, Position as TaoPosition, Size as TaoSize,
49+
PhysicalPosition as TaoPhysicalPosition, PhysicalSize as TaoPhysicalSize,
50+
Position as TaoPosition, Size as TaoSize,
5151
},
5252
event::{Event, StartCause, WindowEvent as TaoWindowEvent},
5353
event_loop::{
@@ -793,16 +793,16 @@ impl WindowBuilder for WindowBuilderWrapper {
793793
let mut constraints = WindowSizeConstraints::default();
794794

795795
if let Some(min_width) = config.min_width {
796-
constraints.min_width = Some(ToaLogicalUnit::new(min_width).into());
796+
constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into());
797797
}
798798
if let Some(min_height) = config.min_height {
799-
constraints.min_height = Some(ToaLogicalUnit::new(min_height).into());
799+
constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into());
800800
}
801801
if let Some(max_width) = config.max_width {
802-
constraints.max_width = Some(ToaLogicalUnit::new(max_width).into());
802+
constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into());
803803
}
804804
if let Some(max_height) = config.max_height {
805-
constraints.max_height = Some(ToaLogicalUnit::new(max_height).into());
805+
constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into());
806806
}
807807
window = window.inner_size_constraints(constraints);
808808

core/tauri-utils/src/config.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2170,7 +2170,9 @@ where
21702170
/// - [`bundle`](#bundleconfig): The bundle configurations
21712171
/// - [`plugins`](#pluginconfig): The plugins configuration
21722172
///
2173-
/// ```json title="Example tauri.config.json file"
2173+
/// Example tauri.config.json file:
2174+
///
2175+
/// ```json
21742176
/// {
21752177
/// "productName": "tauri-app",
21762178
/// "version": "0.1.0",

core/tauri/src/app.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,6 +1741,13 @@ tauri::Builder::default()
17411741
self.invoke_key,
17421742
));
17431743

1744+
#[cfg(any(
1745+
target_os = "linux",
1746+
target_os = "dragonfly",
1747+
target_os = "freebsd",
1748+
target_os = "netbsd",
1749+
target_os = "openbsd"
1750+
))]
17441751
let app_id = if manager.config.app.enable_gtk_app_id {
17451752
Some(manager.config.identifier.clone())
17461753
} else {

core/tauri/src/ipc/channel.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020
Manager, Runtime, State, Webview,
2121
};
2222

23-
use super::{CallbackFn, InvokeBody, InvokeError, IpcResponse, Request, Response};
23+
use super::{CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response};
2424

2525
pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:";
2626
pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__";
@@ -33,13 +33,13 @@ static CHANNEL_DATA_COUNTER: AtomicU32 = AtomicU32::new(0);
3333

3434
/// Maps a channel id to a pending data that must be send to the JavaScript side via the IPC.
3535
#[derive(Default, Clone)]
36-
pub struct ChannelDataIpcQueue(pub(crate) Arc<Mutex<HashMap<u32, InvokeBody>>>);
36+
pub struct ChannelDataIpcQueue(pub(crate) Arc<Mutex<HashMap<u32, InvokeResponseBody>>>);
3737

3838
/// An IPC channel.
3939
#[derive(Clone)]
40-
pub struct Channel<TSend = InvokeBody> {
40+
pub struct Channel<TSend = InvokeResponseBody> {
4141
id: u32,
42-
on_message: Arc<dyn Fn(InvokeBody) -> crate::Result<()> + Send + Sync>,
42+
on_message: Arc<dyn Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync>,
4343
phantom: std::marker::PhantomData<TSend>,
4444
}
4545

@@ -138,13 +138,13 @@ impl<'de> Deserialize<'de> for JavaScriptChannelId {
138138

139139
impl<TSend> Channel<TSend> {
140140
/// Creates a new channel with the given message handler.
141-
pub fn new<F: Fn(InvokeBody) -> crate::Result<()> + Send + Sync + 'static>(
141+
pub fn new<F: Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync + 'static>(
142142
on_message: F,
143143
) -> Self {
144144
Self::new_with_id(CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed), on_message)
145145
}
146146

147-
fn new_with_id<F: Fn(InvokeBody) -> crate::Result<()> + Send + Sync + 'static>(
147+
fn new_with_id<F: Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync + 'static>(
148148
id: u32,
149149
on_message: F,
150150
) -> Self {
@@ -195,8 +195,7 @@ impl<TSend> Channel<TSend> {
195195
where
196196
TSend: IpcResponse,
197197
{
198-
let body = data.body()?;
199-
(self.on_message)(body)
198+
(self.on_message)(data.body()?)
200199
}
201200
}
202201

core/tauri/src/ipc/command.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> {
183183
#[doc(hidden)]
184184
pub mod private {
185185
use crate::{
186-
ipc::{InvokeBody, InvokeError, InvokeResolver, IpcResponse},
186+
ipc::{InvokeError, InvokeResolver, InvokeResponseBody, IpcResponse},
187187
Runtime,
188188
};
189189
use futures_util::{FutureExt, TryFutureExt};
@@ -220,7 +220,10 @@ pub mod private {
220220
}
221221

222222
#[inline(always)]
223-
pub fn future<T>(self, value: T) -> impl Future<Output = Result<InvokeBody, InvokeError>>
223+
pub fn future<T>(
224+
self,
225+
value: T,
226+
) -> impl Future<Output = Result<InvokeResponseBody, InvokeError>>
224227
where
225228
T: IpcResponse,
226229
{
@@ -261,7 +264,7 @@ pub mod private {
261264
pub fn future<T, E>(
262265
self,
263266
value: Result<T, E>,
264-
) -> impl Future<Output = Result<InvokeBody, InvokeError>>
267+
) -> impl Future<Output = Result<InvokeResponseBody, InvokeError>>
265268
where
266269
T: IpcResponse,
267270
E: Into<InvokeError>,
@@ -288,7 +291,10 @@ pub mod private {
288291

289292
impl FutureTag {
290293
#[inline(always)]
291-
pub fn future<T, F>(self, value: F) -> impl Future<Output = Result<InvokeBody, InvokeError>>
294+
pub fn future<T, F>(
295+
self,
296+
value: F,
297+
) -> impl Future<Output = Result<InvokeResponseBody, InvokeError>>
292298
where
293299
T: IpcResponse,
294300
F: Future<Output = T> + Send + 'static,
@@ -315,7 +321,10 @@ pub mod private {
315321

316322
impl ResultFutureTag {
317323
#[inline(always)]
318-
pub fn future<T, E, F>(self, value: F) -> impl Future<Output = Result<InvokeBody, InvokeError>>
324+
pub fn future<T, E, F>(
325+
self,
326+
value: F,
327+
) -> impl Future<Output = Result<InvokeResponseBody, InvokeError>>
319328
where
320329
T: IpcResponse,
321330
E: Into<InvokeError>,

core/tauri/src/ipc/format_callback.rs

Lines changed: 133 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ const MIN_JSON_PARSE_LEN: usize = 10_240;
4040
/// 1. `serde_json`'s ability to correctly escape and format json into a string.
4141
/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
4242
/// character to end a string that was opened with it.
43-
fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
44-
value: &T,
43+
fn serialize_js_with<F: FnOnce(&str) -> String>(
44+
json_string: String,
4545
options: serialize_to_javascript::Options,
4646
cb: F,
4747
) -> crate::Result<String> {
4848
// get a raw &str representation of a serialized json value.
49-
let string = serde_json::to_string(value)?;
50-
let raw = RawValue::from_string(string)?;
49+
50+
let raw = RawValue::from_string(json_string)?;
5151

5252
// from here we know json.len() > 1 because an empty string is not a valid json value.
5353
let json = raw.get();
@@ -77,14 +77,21 @@ fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
7777
Ok(return_val)
7878
}
7979

80-
/// Formats a function name and argument to be evaluated as callback.
80+
/// Formats a function name and a serializable argument to be evaluated as callback.
81+
///
82+
/// See [`format_raw`] for more information.
83+
pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result<String> {
84+
format_raw(function_name, serde_json::to_string(arg)?)
85+
}
86+
87+
/// Formats a function name and a raw JSON string argument to be evaluated as callback.
8188
///
8289
/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals,
8390
/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger
8491
/// than 10 KiB with `JSON.parse('...')`.
8592
/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
86-
pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result<String> {
87-
serialize_js_with(arg, Default::default(), |arg| {
93+
pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result<String> {
94+
serialize_js_with(json_string, Default::default(), |arg| {
8895
format!(
8996
r#"
9097
if (window["_{fn}"]) {{
@@ -97,7 +104,21 @@ pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result
97104
})
98105
}
99106

100-
/// Formats a Result type to its Promise response.
107+
/// Formats a serializable Result type to its Promise response.
108+
///
109+
/// See [`format_result_raw`] for more information.
110+
pub fn format_result<T: Serialize, E: Serialize>(
111+
result: Result<T, E>,
112+
success_callback: CallbackFn,
113+
error_callback: CallbackFn,
114+
) -> crate::Result<String> {
115+
match result {
116+
Ok(res) => format(success_callback, &res),
117+
Err(err) => format(error_callback, &err),
118+
}
119+
}
120+
121+
/// Formats a Result type of raw JSON strings to its Promise response.
101122
/// Useful for Promises handling.
102123
/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
103124
/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
@@ -107,14 +128,14 @@ pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result
107128
/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
108129
///
109130
/// Note that the callback strings are automatically generated by the `invoke` helper.
110-
pub fn format_result<T: Serialize, E: Serialize>(
111-
result: Result<T, E>,
131+
pub fn format_result_raw(
132+
raw_result: Result<String, String>,
112133
success_callback: CallbackFn,
113134
error_callback: CallbackFn,
114135
) -> crate::Result<String> {
115-
match result {
116-
Ok(res) => format(success_callback, &res),
117-
Err(err) => format(error_callback, &err),
136+
match raw_result {
137+
Ok(res) => format_raw(success_callback, res),
138+
Err(err) => format_raw(error_callback, err),
118139
}
119140
}
120141

@@ -130,8 +151,31 @@ mod test {
130151
}
131152
}
132153

154+
#[derive(Debug, Clone)]
155+
struct JsonStr(String);
156+
157+
impl Arbitrary for JsonStr {
158+
fn arbitrary(g: &mut Gen) -> Self {
159+
if bool::arbitrary(g) {
160+
Self(format!(
161+
"{{ {}: {} }}",
162+
serde_json::to_string(&String::arbitrary(g)).unwrap(),
163+
serde_json::to_string(&String::arbitrary(g)).unwrap()
164+
))
165+
} else {
166+
Self(serde_json::to_string(&String::arbitrary(g)).unwrap())
167+
}
168+
}
169+
}
170+
133171
fn serialize_js<T: Serialize>(value: &T) -> crate::Result<String> {
134-
serialize_js_with(value, Default::default(), |v| v.into())
172+
serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| {
173+
v.into()
174+
})
175+
}
176+
177+
fn serialize_js_raw(value: impl Into<String>) -> crate::Result<String> {
178+
serialize_js_with(value.into(), Default::default(), |v| v.into())
135179
}
136180

137181
#[test]
@@ -213,4 +257,79 @@ mod test {
213257
serde_json::Value::String(value),
214258
))
215259
}
260+
261+
#[test]
262+
fn test_serialize_js_raw() {
263+
assert_eq!(serialize_js_raw("null").unwrap(), "null");
264+
assert_eq!(serialize_js_raw("5").unwrap(), "5");
265+
assert_eq!(
266+
serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(),
267+
"{ \"x\": [1, 2, 3] }"
268+
);
269+
270+
#[derive(serde::Serialize)]
271+
struct JsonObj {
272+
value: String,
273+
}
274+
275+
let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
276+
assert_eq!(
277+
serialize_js_raw(format!("\"{raw_str}\"")).unwrap(),
278+
format!("\"{raw_str}\"")
279+
);
280+
281+
assert_eq!(
282+
serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(),
283+
format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
284+
);
285+
286+
assert_eq!(
287+
serialize_js(&JsonObj {
288+
value: format!("\"{raw_str}\"")
289+
})
290+
.unwrap(),
291+
format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
292+
);
293+
294+
let dangerous_json = RawValue::from_string(
295+
r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into()
296+
).unwrap();
297+
298+
let definitely_escaped_dangerous_json = format!(
299+
"JSON.parse('{}')",
300+
dangerous_json
301+
.get()
302+
.replace('\\', "\\\\")
303+
.replace('\'', "\\'")
304+
);
305+
let escape_single_quoted_json_test =
306+
serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string();
307+
308+
let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#;
309+
assert_eq!(definitely_escaped_dangerous_json, result);
310+
assert_eq!(escape_single_quoted_json_test, result);
311+
}
312+
313+
// check arbitrary strings in the format callback function
314+
#[quickcheck]
315+
fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool {
316+
let a = a.0;
317+
// call format callback
318+
let fc = format_raw(f, a.clone()).unwrap();
319+
fc.contains(&format!(r#"window["_{}"](JSON.parse('{}'))"#, f.0, a))
320+
|| fc.contains(&format!(r#"window["_{}"]({})"#, f.0, a))
321+
}
322+
323+
// check arbitrary strings in format_result
324+
#[quickcheck]
325+
fn qc_format_raw_res(result: Result<JsonStr, JsonStr>, c: CallbackFn, ec: CallbackFn) -> bool {
326+
let result = result.map(|v| v.0).map_err(|e| e.0);
327+
let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result");
328+
let (function, value) = match result {
329+
Ok(v) => (c, v),
330+
Err(e) => (ec, e),
331+
};
332+
333+
resp.contains(&format!(r#"window["_{}"]({})"#, function.0, value))
334+
}
216335
}

0 commit comments

Comments
 (0)