calling-rust-from-tauri-frontend

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Calling Rust from Tauri Frontend

从Tauri前端调用Rust代码

This skill covers how to call Rust backend functions from your Tauri v2 frontend using the command system and invoke function.
本技能介绍如何使用命令系统和invoke函数从Tauri v2前端调用Rust后端函数。

Overview

概述

Tauri provides two IPC mechanisms:
  • Commands (recommended): Type-safe function calls with serialized arguments/return values
  • Events: Dynamic, one-way communication (not covered here)
Tauri提供两种IPC机制:
  • 命令(推荐):类型安全的函数调用,支持参数/返回值序列化
  • 事件:动态的单向通信(本文不涉及)

Basic Commands

基础命令

Defining a Command in Rust

在Rust中定义命令

Use the
#[tauri::command]
attribute macro:
rust
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}
使用
#[tauri::command]
属性宏:
rust
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}

Registering Commands

注册命令

Commands must be registered with the invoke handler:
rust
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, login, fetch_data])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}
命令必须注册到invoke处理器中:
rust
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, login, fetch_data])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}

Invoking from JavaScript/TypeScript

在JavaScript/TypeScript中调用

typescript
import { invoke } from '@tauri-apps/api/core';

const greeting = await invoke('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"
Or with the global Tauri object (when
app.withGlobalTauri
is enabled):
javascript
const { invoke } = window.__TAURI__.core;
const greeting = await invoke('greet', { name: 'World' });
typescript
import { invoke } from '@tauri-apps/api/core';

const greeting = await invoke('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"
或者使用全局Tauri对象(当
app.withGlobalTauri
启用时):
javascript
const { invoke } = window.__TAURI__.core;
const greeting = await invoke('greet', { name: 'World' });

Passing Arguments

传递参数

Argument Naming Convention

参数命名规范

By default, Rust snake_case arguments map to JavaScript camelCase:
rust
#[tauri::command]
fn create_user(user_name: String, user_age: u32) -> String {
    format!("{} is {} years old", user_name, user_age)
}
typescript
await invoke('create_user', { userName: 'Alice', userAge: 30 });
Use
rename_all
to change the naming convention:
rust
#[tauri::command(rename_all = "snake_case")]
fn create_user(user_name: String, user_age: u32) -> String {
    format!("{} is {} years old", user_name, user_age)
}
默认情况下,Rust的snake_case参数会映射到JavaScript的camelCase:
rust
#[tauri::command]
fn create_user(user_name: String, user_age: u32) -> String {
    format!("{} is {} years old", user_name, user_age)
}
typescript
await invoke('create_user', { userName: 'Alice', userAge: 30 });
使用
rename_all
修改命名规范:
rust
#[tauri::command(rename_all = "snake_case")]
fn create_user(user_name: String, user_age: u32) -> String {
    format!("{} is {} years old", user_name, user_age)
}

Complex Arguments

复杂参数

Arguments must implement
serde::Deserialize
:
rust
use serde::Deserialize;

#[derive(Deserialize)]
struct UserData {
    name: String,
    email: String,
    age: u32,
}

#[tauri::command]
fn register_user(user: UserData) -> String {
    format!("Registered {} ({}) age {}", user.name, user.email, user.age)
}
typescript
await invoke('register_user', {
    user: { name: 'Alice', email: 'alice@example.com', age: 30 }
});
参数必须实现
serde::Deserialize
rust
use serde::Deserialize;

#[derive(Deserialize)]
struct UserData {
    name: String,
    email: String,
    age: u32,
}

#[tauri::command]
fn register_user(user: UserData) -> String {
    format!("Registered {} ({}) age {}", user.name, user.email, user.age)
}
typescript
await invoke('register_user', {
    user: { name: 'Alice', email: 'alice@example.com', age: 30 }
});

Returning Values

返回值

Simple Return Types

简单返回类型

Return types must implement
serde::Serialize
:
rust
#[tauri::command]
fn get_count() -> i32 { 42 }

#[tauri::command]
fn get_message() -> String { "Hello from Rust!".into() }
typescript
const count: number = await invoke('get_count');
const message: string = await invoke('get_message');
返回类型必须实现
serde::Serialize
rust
#[tauri::command]
fn get_count() -> i32 { 42 }

#[tauri::command]
fn get_message() -> String { "Hello from Rust!".into() }
typescript
const count: number = await invoke('get_count');
const message: string = await invoke('get_message');

Returning Complex Types

返回复杂类型

rust
use serde::Serialize;

#[derive(Serialize)]
struct AppConfig {
    theme: String,
    language: String,
    notifications_enabled: bool,
}

#[tauri::command]
fn get_config() -> AppConfig {
    AppConfig {
        theme: "dark".into(),
        language: "en".into(),
        notifications_enabled: true,
    }
}
typescript
interface AppConfig {
    theme: string;
    language: string;
    notificationsEnabled: boolean;
}
const config: AppConfig = await invoke('get_config');
rust
use serde::Serialize;

#[derive(Serialize)]
struct AppConfig {
    theme: String,
    language: String,
    notifications_enabled: bool,
}

#[tauri::command]
fn get_config() -> AppConfig {
    AppConfig {
        theme: "dark".into(),
        language: "en".into(),
        notifications_enabled: true,
    }
}
typescript
interface AppConfig {
    theme: string;
    language: string;
    notificationsEnabled: boolean;
}
const config: AppConfig = await invoke('get_config');

Returning Binary Data

返回二进制数据

For large binary data, use
tauri::ipc::Response
to bypass JSON serialization:
rust
use tauri::ipc::Response;

#[tauri::command]
fn read_file(path: String) -> Response {
    let data = std::fs::read(&path).unwrap();
    Response::new(data)
}
typescript
const data: ArrayBuffer = await invoke('read_file', { path: '/path/to/file' });
对于大型二进制数据,使用
tauri::ipc::Response
绕过JSON序列化:
rust
use tauri::ipc::Response;

#[tauri::command]
fn read_file(path: String) -> Response {
    let data = std::fs::read(&path).unwrap();
    Response::new(data)
}
typescript
const data: ArrayBuffer = await invoke('read_file', { path: '/path/to/file' });

Error Handling

错误处理

Using Result Types

使用Result类型

Return
Result<T, E>
where
E
implements
Serialize
or is a
String
:
rust
#[tauri::command]
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Cannot divide by zero".into())
    } else {
        Ok(a / b)
    }
}
typescript
try {
    const result = await invoke('divide', { a: 10, b: 0 });
} catch (error) {
    console.error('Error:', error); // "Cannot divide by zero"
}
返回
Result<T, E>
,其中
E
需要实现
Serialize
或为
String
类型:
rust
#[tauri::command]
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Cannot divide by zero".into())
    } else {
        Ok(a / b)
    }
}
typescript
try {
    const result = await invoke('divide', { a: 10, b: 0 });
} catch (error) {
    console.error('Error:', error); // "Cannot divide by zero"
}

Custom Error Types with thiserror

使用thiserror定义自定义错误类型

rust
use serde::Serialize;
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("File not found: {0}")]
    FileNotFound(String),
    #[error("Permission denied")]
    PermissionDenied,
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

impl Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where S: serde::ser::Serializer {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[tauri::command]
fn open_file(path: String) -> Result<String, AppError> {
    if !std::path::Path::new(&path).exists() {
        return Err(AppError::FileNotFound(path));
    }
    let content = std::fs::read_to_string(&path)?;
    Ok(content)
}
rust
use serde::Serialize;
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("File not found: {0}")]
    FileNotFound(String),
    #[error("Permission denied")]
    PermissionDenied,
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

impl Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where S: serde::ser::Serializer {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[tauri::command]
fn open_file(path: String) -> Result<String, AppError> {
    if !std::path::Path::new(&path).exists() {
        return Err(AppError::FileNotFound(path));
    }
    let content = std::fs::read_to_string(&path)?;
    Ok(content)
}

Structured Error Responses

结构化错误响应

rust
use serde::Serialize;

#[derive(Debug, Serialize)]
struct ErrorResponse { code: String, message: String }

#[tauri::command]
fn validate_input(input: String) -> Result<String, ErrorResponse> {
    if input.is_empty() {
        return Err(ErrorResponse {
            code: "EMPTY_INPUT".into(),
            message: "Input cannot be empty".into(),
        });
    }
    Ok(input.to_uppercase())
}
typescript
interface ErrorResponse { code: string; message: string; }

try {
    const result = await invoke('validate_input', { input: '' });
} catch (error) {
    const err = error as ErrorResponse;
    console.error(`Error ${err.code}: ${err.message}`);
}
rust
use serde::Serialize;

#[derive(Debug, Serialize)]
struct ErrorResponse { code: String, message: String }

#[tauri::command]
fn validate_input(input: String) -> Result<String, ErrorResponse> {
    if input.is_empty() {
        return Err(ErrorResponse {
            code: "EMPTY_INPUT".into(),
            message: "Input cannot be empty".into(),
        });
    }
    Ok(input.to_uppercase())
}
typescript
interface ErrorResponse { code: string; message: string; }

try {
    const result = await invoke('validate_input', { input: '' });
} catch (error) {
    const err = error as ErrorResponse;
    console.error(`Error ${err.code}: ${err.message}`);
}

Async Commands

异步命令

Defining Async Commands

定义异步命令

Use the
async
keyword:
rust
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
    let body = response.text().await.map_err(|e| e.to_string())?;
    Ok(body)
}
使用
async
关键字:
rust
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
    let body = response.text().await.map_err(|e| e.to_string())?;
    Ok(body)
}

Async with Borrowed Types Limitation

异步命令的借用类型限制

Async commands cannot use borrowed types like
&str
directly:
rust
// Will NOT compile:
// async fn bad_command(value: &str) -> String { ... }

// Use owned types instead:
#[tauri::command]
async fn good_command(value: String) -> String {
    some_async_operation(&value).await;
    value
}

// Or wrap in Result as workaround:
#[tauri::command]
async fn with_borrowed(value: &str) -> Result<String, ()> {
    some_async_operation(value).await;
    Ok(value.to_string())
}
异步命令不能直接使用
&str
等借用类型:
rust
// 无法编译:
// async fn bad_command(value: &str) -> String { ... }

// 改用拥有类型:
#[tauri::command]
async fn good_command(value: String) -> String {
    some_async_operation(&value).await;
    value
}

// 或者用Result作为变通方案:
#[tauri::command]
async fn with_borrowed(value: &str) -> Result<String, ()> {
    some_async_operation(value).await;
    Ok(value.to_string())
}

Frontend Invocation

前端调用

Async commands work identically to sync since
invoke
returns a Promise:
typescript
const result = await invoke('fetch_data', { url: 'https://api.example.com/data' });
异步命令的调用方式与同步命令相同,因为
invoke
返回Promise:
typescript
const result = await invoke('fetch_data', { url: 'https://api.example.com/data' });

Accessing Tauri Internals

访问Tauri内部资源

WebviewWindow, AppHandle, and State

WebviewWindow、AppHandle和State

rust
use std::sync::Mutex;

struct AppState { counter: Mutex<i32> }

#[tauri::command]
async fn get_window_label(window: tauri::WebviewWindow) -> String {
    window.label().to_string()
}

#[tauri::command]
async fn get_app_version(app: tauri::AppHandle) -> String {
    app.package_info().version.to_string()
}

#[tauri::command]
fn increment_counter(state: tauri::State<AppState>) -> i32 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

pub fn run() {
    tauri::Builder::default()
        .manage(AppState { counter: Mutex::new(0) })
        .invoke_handler(tauri::generate_handler![
            get_window_label, get_app_version, increment_counter
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}
rust
use std::sync::Mutex;

struct AppState { counter: Mutex<i32> }

#[tauri::command]
async fn get_window_label(window: tauri::WebviewWindow) -> String {
    window.label().to_string()
}

#[tauri::command]
async fn get_app_version(app: tauri::AppHandle) -> String {
    app.package_info().version.to_string()
}

#[tauri::command]
fn increment_counter(state: tauri::State<AppState>) -> i32 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

pub fn run() {
    tauri::Builder::default()
        .manage(AppState { counter: Mutex::new(0) })
        .invoke_handler(tauri::generate_handler![
            get_window_label, get_app_version, increment_counter
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}

Advanced Features

高级特性

Raw Request Access

访问原始请求

Access headers and raw body:
rust
use tauri::ipc::{Request, InvokeBody};

#[tauri::command]
fn upload(request: Request) -> Result<String, String> {
    let InvokeBody::Raw(data) = request.body() else {
        return Err("Expected raw body".into());
    };
    let auth = request.headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .ok_or("Missing Authorization header")?;
    Ok(format!("Received {} bytes", data.len()))
}
typescript
const data = new Uint8Array([1, 2, 3, 4, 5]);
await invoke('upload', data, { headers: { Authorization: 'Bearer token123' } });
访问请求头和原始请求体:
rust
use tauri::ipc::{Request, InvokeBody};

#[tauri::command]
fn upload(request: Request) -> Result<String, String> {
    let InvokeBody::Raw(data) = request.body() else {
        return Err("Expected raw body".into());
    };
    let auth = request.headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .ok_or("Missing Authorization header")?;
    Ok(format!("Received {} bytes", data.len()))
}
typescript
const data = new Uint8Array([1, 2, 3, 4, 5]);
await invoke('upload', data, { headers: { Authorization: 'Bearer token123' } });

Channels for Streaming

使用Channel实现流式传输

rust
use tauri::ipc::Channel;
use tokio::io::AsyncReadExt;

#[tauri::command]
async fn stream_file(path: String, channel: Channel<Vec<u8>>) -> Result<(), String> {
    let mut file = tokio::fs::File::open(&path).await.map_err(|e| e.to_string())?;
    let mut buffer = vec![0u8; 4096];
    loop {
        let len = file.read(&mut buffer).await.map_err(|e| e.to_string())?;
        if len == 0 { break; }
        channel.send(buffer[..len].to_vec()).map_err(|e| e.to_string())?;
    }
    Ok(())
}
typescript
import { Channel } from '@tauri-apps/api/core';

const channel = new Channel<Uint8Array>();
channel.onmessage = (chunk) => console.log('Received:', chunk.length, 'bytes');
await invoke('stream_file', { path: '/path/to/file', channel });
rust
use tauri::ipc::Channel;
use tokio::io::AsyncReadExt;

#[tauri::command]
async fn stream_file(path: String, channel: Channel<Vec<u8>>) -> Result<(), String> {
    let mut file = tokio::fs::File::open(&path).await.map_err(|e| e.to_string())?;
    let mut buffer = vec![0u8; 4096];
    loop {
        let len = file.read(&mut buffer).await.map_err(|e| e.to_string())?;
        if len == 0 { break; }
        channel.send(buffer[..len].to_vec()).map_err(|e| e.to_string())?;
    }
    Ok(())
}
typescript
import { Channel } from '@tauri-apps/api/core';

const channel = new Channel<Uint8Array>();
channel.onmessage = (chunk) => console.log('Received:', chunk.length, 'bytes');
await invoke('stream_file', { path: '/path/to/file', channel });

Organizing Commands in Modules

模块化组织命令

rust
// src-tauri/src/commands/user.rs
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct CreateUserRequest { pub name: String, pub email: String }

#[derive(Serialize)]
pub struct User { pub id: u32, pub name: String, pub email: String }

#[tauri::command]
pub fn create_user(request: CreateUserRequest) -> User {
    User { id: 1, name: request.name, email: request.email }
}
rust
// src-tauri/src/commands/mod.rs
pub mod user;
rust
// src-tauri/src/lib.rs
mod commands;

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![commands::user::create_user])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}
rust
// src-tauri/src/commands/user.rs
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct CreateUserRequest { pub name: String, pub email: String }

#[derive(Serialize)]
pub struct User { pub id: u32, pub name: String, pub email: String }

#[tauri::command]
pub fn create_user(request: CreateUserRequest) -> User {
    User { id: 1, name: request.name, email: request.email }
}
rust
// src-tauri/src/commands/mod.rs
pub mod user;
rust
// src-tauri/src/lib.rs
mod commands;

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![commands::user::create_user])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}

TypeScript Type Safety

TypeScript类型安全

Create a typed wrapper:
typescript
import { invoke } from '@tauri-apps/api/core';

export interface User { id: number; name: string; email: string; }
export interface CreateUserRequest { name: string; email: string; }

export const commands = {
    createUser: (request: CreateUserRequest): Promise<User> =>
        invoke('create_user', { request }),
    greet: (name: string): Promise<string> =>
        invoke('greet', { name }),
};

// Usage
const user = await commands.createUser({ name: 'Bob', email: 'bob@example.com' });
创建类型化的封装函数:
typescript
import { invoke } from '@tauri-apps/api/core';

export interface User { id: number; name: string; email: string; }
export interface CreateUserRequest { name: string; email: string; }

export const commands = {
    createUser: (request: CreateUserRequest): Promise<User> =>
        invoke('create_user', { request }),
    greet: (name: string): Promise<string> =>
        invoke('greet', { name }),
};

// 使用示例
const user = await commands.createUser({ name: 'Bob', email: 'bob@example.com' });

Quick Reference

快速参考

TaskRustJavaScript
Define command
#[tauri::command] fn name() {}
-
Register command
tauri::generate_handler![name]
-
Invoke command-
await invoke('name', { args })
Return value
-> T
where T: Serialize
const result = await invoke(...)
Return error
-> Result<T, E>
try/catch
Async command
async fn name()
Same as sync
Access window
window: tauri::WebviewWindow
-
Access app
app: tauri::AppHandle
-
Access state
state: tauri::State<T>
-
任务Rust代码JavaScript代码
定义命令
#[tauri::command] fn name() {}
-
注册命令
tauri::generate_handler![name]
-
调用命令-
await invoke('name', { args })
返回值
-> T
其中T实现Serialize
const result = await invoke(...)
返回错误
-> Result<T, E>
try/catch
异步命令
async fn name()
与同步命令相同
访问窗口
window: tauri::WebviewWindow
-
访问应用实例
app: tauri::AppHandle
-
访问状态
state: tauri::State<T>
-

Key Constraints

关键约束

  1. Command names must be unique across the entire application
  2. Commands in
    lib.rs
    cannot be
    pub
    (use modules for organization)
  3. All commands must be registered in a single
    generate_handler!
    call
  4. Async commands cannot use borrowed types like
    &str
    directly
  5. Arguments must implement
    Deserialize
    , return types must implement
    Serialize
  1. 命令名称在整个应用中必须唯一
  2. lib.rs中的命令不能设为pub(使用模块组织命令)
  3. 所有命令必须在同一个
    generate_handler!
    调用中注册
  4. 异步命令不能直接使用
    &str
    等借用类型
  5. 参数必须实现
    Deserialize
    ,返回类型必须实现
    Serialize