IPC bindings for using Tauri with a Rust Frontend (e.g. leptos).
NOTE: The API is currently unstable and may change.
I couldn't find a comfortable way of defining commands that would maintain type safety with Tauri IPC bindings for a Rust Frontend. So this is a crude attempt at solving this without changing too much about how the commands are defined.
-
Create an intermediary crate in the workspace of your Tauri app to house traits defining your commands, events, and generated IPC bindings to import into the Rust frontend, e.g:
[package] edition = "2021" name = "my-commands" version = "0.1.0" [dependencies] tauri-ipc-macros = { version = "0.1.2", git = "https://github.com/jvatic/tauri-ipc-macros.git" } serde = { version = "1.0.204", features = ["derive"] } serde-wasm-bindgen = "0.6" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4"
#[allow(async_fn_in_trait)] #[tauri_bindgen_rs_macros::invoke_bindings] pub trait Commands { async hello(name: String) -> Result<String, String>; } #[derive(tauri_bindgen_rs_macros::Events, Debug, Clone, ::serde::Serialize, ::serde::Deserialize)] enum Event { SomethingHappened { payload: Vec<u8> }, SomeoneSaidHello(String), NoPayload, }
NOTE: If you have multiple enums deriving
Events
, these will need to be in separate modules since there's some common boilerplate types that are included currently (that will be moved into another crate at some point).And if you're using a plugin on the frontend and want bindings generated for it, you can do so by defining a trait for it, e.g:
pub mod someplugin { #[allow(async_fn_in_trait)] #[tauri_bindgen_rs_macros::invoke_bindings(cmd_prefix = "plugin:some-plugin|")] pub trait SomePlugin { // ... } }
NOTE: You can find the
cmd_prefix
and plugin API by looking at the guest-js bindings and Rust source for the plugin(s) you're using.NOTE: If you have multiple traits implementing
invoke_bindings
they'll each need to be in their ownmod
since aninvoke
WASM binding will be derived in scope of where the trait is defined (this will be moved into another module at some point). -
Import the commands trait into your Tauri backend and wrap your command definitions in the
impl_trait
macro, e.g:use my_commands::Commands; tauri_bindgen_rs_macros::impl_trait!(Commands, { #[tauri::command] async hello(state: tauri::State, name: String) -> Result<String, String> { Ok(format!("Hello {}", name)) } });
This will define a new struct named
__ImplCommands
with animpl Commands for __ImplCommands
block with all the fns passed into the macro (minus any fn generics or arguments where the type starts withtauri::
), and spits out the actual fns untouched. The Rust compiler will then emit helpful errors if the defined commands are different (after being processed) from those in the trait, yay!NOTE: The crudeness here is due to
#[tauri::command]
s needing to be top level fns and potentially having additional arguments in the siganture. And while I can imagine a way of abstracting this out of the API (so this could be a regularimpl
block), this was the easiest thing and works without changing much about how the commands are defined. -
Import the event enum into your Tauri backend if you wish to emit events from there, e.g.:
use my_commands::Event; fn emit_event(app_handle: tauri::AppHandle, event: Event) -> anyhow::Result<()> { Ok(app_handle.emit(event.event_name(), event)?) }
-
Use the generated IPC bindings in your Rust frontend, eg:
// ... spawn_local(async move { let greeting = my_commands::hello(name).await.unwrap(); set_greeting(greeting); }); // ... spawn_local(async move { let listener = my_commands::EventBinding::SomethingHappened.listen(|event: my_commands::Event| { // ... }).await; drop(listener); // unlisten }); // ...