calling-rust-from-tauri-frontend by dchuk/claude-code-tauri-skills
npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill calling-rust-from-tauri-frontend本技能涵盖如何使用命令系统和调用函数从 Tauri v2 前端调用 Rust 后端函数。
Tauri 提供两种 IPC 机制:
使用 #[tauri::command] 属性宏:
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
命令必须向调用处理器注册:
#[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")
}
import { invoke } from '@tauri-apps/api/core';
const greeting = await invoke('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
或者使用全局 Tauri 对象(当启用 app.withGlobalTauri 时):
const { invoke } = window.__TAURI__.core;
const greeting = await invoke('greet', { name: 'World' });
默认情况下,Rust 的 snake_case 参数映射到 JavaScript 的 camelCase:
#[tauri::command]
fn create_user(user_name: String, user_age: u32) -> String {
format!("{} is {} years old", user_name, user_age)
}
await invoke('create_user', { userName: 'Alice', userAge: 30 });
使用 rename_all 来更改命名约定:
#[tauri::command(rename_all = "snake_case")]
fn create_user(user_name: String, user_age: u32) -> String {
format!("{} is {} years old", user_name, user_age)
}
参数必须实现 serde::Deserialize:
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)
}
await invoke('register_user', {
user: { name: 'Alice', email: 'alice@example.com', age: 30 }
});
返回类型必须实现 serde::Serialize:
#[tauri::command]
fn get_count() -> i32 { 42 }
#[tauri::command]
fn get_message() -> String { "Hello from Rust!".into() }
const count: number = await invoke('get_count');
const message: string = await invoke('get_message');
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,
}
}
interface AppConfig {
theme: string;
language: string;
notificationsEnabled: boolean;
}
const config: AppConfig = await invoke('get_config');
对于大型二进制数据,使用 tauri::ipc::Response 来绕过 JSON 序列化:
use tauri::ipc::Response;
#[tauri::command]
fn read_file(path: String) -> Response {
let data = std::fs::read(&path).unwrap();
Response::new(data)
}
const data: ArrayBuffer = await invoke('read_file', { path: '/path/to/file' });
返回 Result<T, E>,其中 E 实现 Serialize 或是 String:
#[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)
}
}
try {
const result = await invoke('divide', { a: 10, b: 0 });
} catch (error) {
console.error('Error:', error); // "Cannot divide by zero"
}
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)
}
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())
}
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 关键字:
#[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)
}
异步命令不能直接使用像 &str 这样的借用类型:
// 将无法编译:
// 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())
}
异步命令的工作方式与同步命令相同,因为 invoke 返回一个 Promise:
const result = await invoke('fetch_data', { url: 'https://api.example.com/data' });
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")
}
访问头部和原始正文:
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()))
}
const data = new Uint8Array([1, 2, 3, 4, 5]);
await invoke('upload', data, { headers: { Authorization: 'Bearer token123' } });
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(())
}
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 });
// 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 }
}
// src-tauri/src/commands/mod.rs
pub mod user;
// 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")
}
创建类型化包装器:
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' });
| 任务 | 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> | - |
lib.rs 中的命令不能是 pub(使用模块进行组织)generate_handler! 调用中注册&str 这样的借用类型Deserialize,返回类型必须实现 Serialize每周安装次数
54
仓库
GitHub 星标数
10
首次出现
2026 年 1 月 24 日
安全审计
安装于
gemini-cli43
opencode42
codex41
cursor40
claude-code39
github-copilot36
This skill covers how to call Rust backend functions from your Tauri v2 frontend using the command system and invoke function.
Tauri provides two IPC mechanisms:
Use the #[tauri::command] attribute macro:
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
Commands must be registered with the invoke handler:
#[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")
}
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):
const { invoke } = window.__TAURI__.core;
const greeting = await invoke('greet', { name: 'World' });
By default, Rust snake_case arguments map to JavaScript camelCase:
#[tauri::command]
fn create_user(user_name: String, user_age: u32) -> String {
format!("{} is {} years old", user_name, user_age)
}
await invoke('create_user', { userName: 'Alice', userAge: 30 });
Use rename_all to change the naming convention:
#[tauri::command(rename_all = "snake_case")]
fn create_user(user_name: String, user_age: u32) -> String {
format!("{} is {} years old", user_name, user_age)
}
Arguments must implement serde::Deserialize:
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)
}
await invoke('register_user', {
user: { name: 'Alice', email: 'alice@example.com', age: 30 }
});
Return types must implement serde::Serialize:
#[tauri::command]
fn get_count() -> i32 { 42 }
#[tauri::command]
fn get_message() -> String { "Hello from Rust!".into() }
const count: number = await invoke('get_count');
const message: string = await invoke('get_message');
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,
}
}
interface AppConfig {
theme: string;
language: string;
notificationsEnabled: boolean;
}
const config: AppConfig = await invoke('get_config');
For large binary data, use tauri::ipc::Response to bypass JSON serialization:
use tauri::ipc::Response;
#[tauri::command]
fn read_file(path: String) -> Response {
let data = std::fs::read(&path).unwrap();
Response::new(data)
}
const data: ArrayBuffer = await invoke('read_file', { path: '/path/to/file' });
Return Result<T, E> where E implements Serialize or is a String:
#[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)
}
}
try {
const result = await invoke('divide', { a: 10, b: 0 });
} catch (error) {
console.error('Error:', error); // "Cannot divide by zero"
}
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)
}
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())
}
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}`);
}
Use the async keyword:
#[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 commands cannot use borrowed types like &str directly:
// 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())
}
Async commands work identically to sync since invoke returns a Promise:
const result = await invoke('fetch_data', { url: 'https://api.example.com/data' });
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")
}
Access headers and raw body:
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()))
}
const data = new Uint8Array([1, 2, 3, 4, 5]);
await invoke('upload', data, { headers: { Authorization: 'Bearer token123' } });
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(())
}
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 });
// 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 }
}
// src-tauri/src/commands/mod.rs
pub mod user;
// 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")
}
Create a typed wrapper:
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' });
| Task | Rust | JavaScript |
|---|---|---|
| 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 |
lib.rs cannot be pub (use modules for organization)generate_handler! call&str directlyDeserialize, return types must implement SerializeWeekly Installs
54
Repository
GitHub Stars
10
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
gemini-cli43
opencode42
codex41
cursor40
claude-code39
github-copilot36
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
122,000 周安装
-> 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> | - |