Tauri IPC Bridge Template
Complete, copy-pasteable tauri_ipc.rs module for a dwind/dominator frontend.
Full Module
#![allow(unused)] fn main() { use serde::de::DeserializeOwned; use wasm_bindgen::prelude::*; // ── Raw JS bindings ────────────────────────────────────────────── #[wasm_bindgen(inline_js = r#" export async function tauri_invoke(cmd, args) { return await window.__TAURI__.core.invoke(cmd, args || {}); } export async function tauri_listen(event, callback) { return await window.__TAURI__.event.listen(event, callback); } export function tauri_convert_file_src(path) { return window.__TAURI__.core.convertFileSrc(path); } export async function tauri_dialog_open(options) { return await window.__TAURI__.dialog.open(options || {}); } export async function tauri_dialog_save(options) { return await window.__TAURI__.dialog.save(options || {}); } "#)] extern "C" { #[wasm_bindgen(catch)] async fn tauri_invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>; #[wasm_bindgen(catch)] async fn tauri_listen( event: &str, callback: &Closure<dyn Fn(JsValue)>, ) -> Result<JsValue, JsValue>; fn tauri_convert_file_src(path: &str) -> String; #[wasm_bindgen(catch)] async fn tauri_dialog_open(options: JsValue) -> Result<JsValue, JsValue>; #[wasm_bindgen(catch)] async fn tauri_dialog_save(options: JsValue) -> Result<JsValue, JsValue>; } // ── Generic helpers ────────────────────────────────────────────── async fn invoke<T: DeserializeOwned>(cmd: &str, args: JsValue) -> Result<T, String> { let result = tauri_invoke(cmd, args) .await .map_err(|e| format!("{:?}", e))?; serde_wasm_bindgen::from_value(result).map_err(|e| e.to_string()) } async fn invoke_unit(cmd: &str, args: JsValue) -> Result<(), String> { tauri_invoke(cmd, args) .await .map_err(|e| format!("{:?}", e))?; Ok(()) } // ── Event listener ─────────────────────────────────────────────── #[derive(serde::Deserialize)] struct EventWrapper<T> { payload: T, } /// Listen for Tauri events emitted by the backend. /// The callback receives the deserialized payload. /// The listener lives for the lifetime of the app. pub fn listen<T: DeserializeOwned + 'static>( event: &str, mut callback: impl FnMut(T) + 'static, ) { let event = event.to_string(); wasm_bindgen_futures::spawn_local(async move { let closure = Closure::new(move |val: JsValue| { match serde_wasm_bindgen::from_value::<EventWrapper<T>>(val) { Ok(wrapper) => callback(wrapper.payload), Err(e) => log::error!("Failed to parse event '{}': {}", "<event>", e), } }); let _ = tauri_listen(&event, &closure).await; closure.forget(); }); } // ── Asset protocol ─────────────────────────────────────────────── /// Convert an absolute file path to an asset:// URL loadable by the webview. /// Requires `protocol-asset` feature and assetProtocol enabled in tauri.conf.json. pub fn convert_file_src(path: &str) -> String { tauri_convert_file_src(path) } // ── File dialogs (requires tauri-plugin-dialog) ────────────────── /// Open a native file picker. Returns the selected file path, or None if cancelled. pub async fn pick_file(title: &str, filters: &[(&str, &[&str])]) -> Result<Option<String>, String> { let filter_array: Vec<serde_json::Value> = filters .iter() .map(|(name, exts)| { serde_json::json!({ "name": name, "extensions": exts, }) }) .collect(); let options = serde_wasm_bindgen::to_value(&serde_json::json!({ "title": title, "filters": filter_array, })) .map_err(|e| e.to_string())?; let result = tauri_dialog_open(options) .await .map_err(|e| format!("{:?}", e))?; if result.is_null() || result.is_undefined() { return Ok(None); } Ok(result.as_string()) } /// Open a native directory picker. Returns the selected path, or None if cancelled. pub async fn pick_directory(title: &str) -> Result<Option<String>, String> { let options = serde_wasm_bindgen::to_value(&serde_json::json!({ "title": title, "directory": true, })) .map_err(|e| e.to_string())?; let result = tauri_dialog_open(options) .await .map_err(|e| format!("{:?}", e))?; if result.is_null() || result.is_undefined() { return Ok(None); } Ok(result.as_string()) } // ── App commands (add your typed wrappers below) ───────────────── // Example: // // #[derive(serde::Deserialize)] // pub struct MyData { // pub name: String, // pub count: u32, // } // // pub async fn get_data() -> Result<MyData, String> { // invoke("get_data", JsValue::NULL).await // } // // pub async fn save_data(name: &str, count: u32) -> Result<(), String> { // let args = serde_wasm_bindgen::to_value(&serde_json::json!({ // "name": name, // camelCase keys for Tauri // "count": count, // })).map_err(|e| e.to_string())?; // invoke_unit("save_data", args).await // } }
Usage Notes
- camelCase args: Tauri deserializes command arguments as camelCase JSON keys, so use
"projectName"not"project_name"in thejson!()macro, even though the backend Rust function usesproject_name: String. Closure::forget(): Event listeners must call.forget()to prevent the closure from being dropped. This leaks memory intentionally — listeners live for the app's lifetime.- Dialog plugin:
pick_fileandpick_directoryrequiretauri-plugin-dialogin the backend and"dialog:default"+"dialog:allow-open"in capabilities. - Asset protocol:
convert_file_srcrequiresfeatures = ["protocol-asset"]on thetauridependency andsecurity.assetProtocol.enable = trueintauri.conf.json.