Inter-Process Communication
We established the general concepts and ideas behind IPC in Background: Inter-Process Communication. This guide teaches you how to put that theory to practice and interact with the IPC system from both JavaScript and Rust.
Events
Events are one-way IPC messages and come in two distinct flavors: Global Events and Window-specific Events. Global events are emitted for application-wide lifecycle events (e.g. update events), while window-specific events are, as the name suggests, emitted for window lifecycle events like resizing, moving, or user-defined events.
Commands
At its simplest, a Command is a Rust function that is invoked in response to an incoming IPC request. This function has access to the application state, windows, may take input parameters, and returns data. You can think of them almost like [Serverless Functions] that live in the Tauri Core process and communicate over IPC.
To turn a Rust function into a Command, add #[tauri::command]
to the
line before fn
. This Attribute Macro wraps your function, handles
JSON serialization, and injects
Special Paramaters.
You can use the invoke()
function provided by the @tauri-apps/api
package to call Commands from the Frontend. The function requires the
Command name and optional parameters and returns a promise that
resolves when the Command finished executing:
Parameters
Commands can have parameters, which are defined like regular Rust function parameters. Tauri will reject IPC requests for a command if the argument number, types, or names are invalid.
All parameters must implement serde::Deserialize
so the
tauri::command
macro can correctly parse the incoming IPC request.
Standard types such as u8
, String
or bool
are deserializable by
default, but you have to derive or manually
implement serde::Deserialize
for
types you defined yourself.
Special Parameters
Commands that only have access to the parameters passed from the
Frontend aren't too helpful, so the tauri::command
macro has a
couple of tricks up its sleeve. If you specify any of the following
types as parameters to your function, they will be automagically
injected by the macro.
tauri::Window
- A handle to the window that invoked the Command.tauri::AppHandle
- A handle to the globalApp
instance.tauri::State<T>
- Tries to inject globally managed stateT
. This requires that you previously calledmanage(T)
.
The tauri::command
macro strips Special Parameters from the function
signature, so they are invisible to the Frontend as Listing 2-TODO
shows.
You can instruct the macro to inject globally managed state by using
the tauri::State
type. This only works with types that you
previously stored in the globaly state with manage()
, see the
State Management guide for more details.
Note: It's an informal convention that you put Special Parameters before any regular Parameters.
Commands with Return Values
Commands can return values to the Frontend, exactly like regular Rust
functions, with one caveat: Return values must be representable as
JSON. In Rust we say that the type needs to implement
serde::Serialize
.
Most standard types such as u8
, String
or bool
already implement
serde::Serialize
by default and even more complex types such as
HashMap<K,V>
can be serialized as long as both generic types
implement serde::Serialize
. For types that you defined yourself you
need to either derive the trait or implement
it manually.
Error handling
Rust has a standard way to represent failures in functions: The
Result<T, E>
type. It is an enum with two variants, Ok(T)
,
representing success, and Err(E)
, representing error.
As you learned earlier Command invocations are represented by a
JavaScript promise. By returning a Result from your Command you can
directly influence the state of that promise: Returning Ok(T)
resolves the promise with the given T
, while returning Err
rejects
the promise with E
as the error.
If you try this using real-world functions, however, you quickly run
into a problem: No error type implements serde::Serialize
!
You could just use an error type, for example, String
like we did in
Listing-TODO, but that is not very idiomatic. Instead, we create a
custom error type that implements serde::Serialize
.
In the
following example, we use a crate called thiserror
to help create
the error type. It allows you to turn enums into error types by
deriving the thiserror::Error
trait. You can consult its
documentation for more details.
// create the error type that represents all errors possible in our program
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error)
}
// we must manually implement serde::Serialize
impl serde::Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
A custom error type has the advantage of making all possible errors
explicit so readers can quickly identify what errors can happen. This
saves other people (and yourself) enormous amounts of time when
reviewing and refactoring code later.
It also gives you full
control over the way your error type gets serialized. In the above
example, we simply returned the error message as a string, but you
could assign each error a code similar to C.
Async Commands
If your Command spends time waiting for IO - maybe it is reading a
file or connecting to a server - it blocks the main process for that
duration. This means the window becomes unresponsive, and your app
freezes. To avoid this problem Rust has builtin support
asynchronous functions through the Future
Trait. A familiar
concept if you already know about Promise
and async/await
in
JavaScript.
You declare an asynchronous command by writing async fn
instead of
fn
:
#[tauri::command]
async fn async_command() {}
Async Commands are executed on a thread pool using
tauri::async_runtime::spawn()
, so long-running tasks no longer
block the Core's main thread. Because Commands map to JavaScript
promises in the Frontend, they also don't block the Frontend's main
thread.
To execute non-async, regular Commands on a different thread, define the macro like so:
#[tauri::command(async)]
.
Listing-TODO shows a more complete example that uses the non-blocking
tokio::fs::read()
function to read a file from disk, convert it to a
Utf8 string and parse it into a Vec of lines. It also uses the
previously introduced thiserror
and serde::Serialize
to create a
custom Error type.