返回 Skill 列表
extension
分类: 开发与工程无需 API Key

tauri-ipc-developer

专门用于在Tauri v2应用程序中实现React前端与Rust后端之间类型安全的IPC通信的代理。在添加新的Tauri命令、实现双向事件、调试IPC序列化问题或优化命令性能时使用。

person作者: jakexiaohubgithub

Tauri IPC Developer

Expert agent for developing type-safe Inter-Process Communication (IPC) between React frontend and Rust backend in Tauri v2 applications.

Core Responsibilities

1. Implement New Tauri Commands

When adding new backend functionality accessible from the frontend:

Rust Side (src-tauri/src/commands.rs):

use tauri::command;
use crate::types::ParametricBand;
use crate::profile::ProfileManager;

#[command]
pub async fn save_profile(
    name: String,
    bands: Vec<ParametricBand>,
    preamp: f32,
) -> Result<String, String> {
    // Implementation
    match ProfileManager::save(&name, bands, preamp) {
        Ok(path) => Ok(path.to_string_lossy().to_string()),
        Err(e) => Err(format!("Failed to save profile: {}", e)),
    }
}

Key Requirements:

  • Use #[command] attribute macro
  • Return Result<T, String> for error handling (String errors appear in frontend)
  • Use async only if the command performs I/O or blocking operations
  • Keep command functions thin - delegate to modules (profile.rs, audio_monitor.rs)
  • Handle all error cases explicitly

Frontend Side (lib/tauri.ts):

import { invoke } from '@tauri-apps/api/core';
import type { ParametricBand } from './types';

export async function saveProfile(
  name: string,
  bands: ParametricBand[],
  preamp: number
): Promise<string> {
  return await invoke<string>('save_profile', { name, bands, preamp });
}

Type Safety Checklist:

  • ✅ TypeScript types match Rust struct definitions
  • ✅ Error handling with try/catch in frontend
  • ✅ Proper serialization of complex types (Vec, HashMap, Option)
  • ✅ Command name matches exactly (snake_case)

2. Type Synchronization

Critical: Frontend and backend types MUST stay in sync.

Rust Types (src-tauri/src/types.rs):

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParametricBand {
    pub filter_type: FilterType,
    pub frequency: f32,
    pub gain: f32,
    pub q_factor: f32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FilterType {
    Peaking,
    LowShelf,
    HighShelf,
}

TypeScript Types (lib/types.ts):

export interface ParametricBand {
  filterType: 'Peaking' | 'LowShelf' | 'HighShelf';
  frequency: number;
  gain: number;
  qFactor: number;
}

export type FilterType = 'Peaking' | 'LowShelf' | 'HighShelf';

Synchronization Rules:

  • Use #[serde(rename_all = "camelCase")] in Rust for JS compatibility
  • Rust f32/f64 → TypeScript number
  • Rust String → TypeScript string
  • Rust Vec<T> → TypeScript T[]
  • Rust Option<T> → TypeScript T | null
  • Rust enums → TypeScript union types

3. Bidirectional Events

For backend → frontend communication (e.g., audio peak meter updates):

Rust Emitter (src-tauri/src/audio_monitor.rs):

use tauri::{AppHandle, Emitter};
use serde::{Serialize, Deserialize};

#[derive(Clone, Serialize, Deserialize)]
pub struct PeakMeterUpdate {
    pub peak_db: f32,
    pub device_name: String,
    pub sample_rate: u32,
}

pub fn emit_peak_update(app: &AppHandle, update: PeakMeterUpdate) {
    let _ = app.emit("peak_meter_update", update);
}

Frontend Listener (lib/use-audio-status.ts):

import { listen } from '@tauri-apps/api/event';
import { useEffect, useState } from 'react';

interface PeakMeterUpdate {
  peakDb: number;
  deviceName: string;
  sampleRate: number;
}

export function useAudioStatus() {
  const [peakData, setPeakData] = useState<PeakMeterUpdate | null>(null);

  useEffect(() => {
    const unlisten = listen<PeakMeterUpdate>('peak_meter_update', (event) => {
      setPeakData(event.payload);
    });

    return () => {
      unlisten.then((fn) => fn());
    };
  }, []);

  return peakData;
}

Event Naming Convention:

  • Use snake_case for event names
  • Prefix domain: audio_*, profile_*, ab_test_*
  • Document event payloads in both codebases

4. Error Handling Patterns

Rust Command Error Handling:

#[command]
pub async fn load_profile(name: String) -> Result<EqProfile, String> {
    ProfileManager::load(&name)
        .map_err(|e| match e.kind() {
            ErrorKind::NotFound => format!("Profile '{}' not found", name),
            ErrorKind::PermissionDenied => "Permission denied".to_string(),
            _ => format!("Failed to load profile: {}", e),
        })
}

Frontend Error Handling:

try {
  const profile = await loadProfile(name);
  setCurrentProfile(profile);
} catch (error) {
  console.error('Load failed:', error);
  toast.error(error as string); // Tauri errors are strings
}

Error Best Practices:

  • Return user-friendly error messages from Rust
  • Log detailed errors server-side before converting to strings
  • Use thiserror crate for structured Rust errors
  • Catch and display errors in UI (toast notifications)

5. Performance Optimization

Debouncing Frequent Commands:

For real-time EQ adjustments, debounce on frontend:

import { debounce } from 'lodash-es';

const debouncedApply = useMemo(
  () =>
    debounce(async (bands: ParametricBand[], preamp: number) => {
      await applyProfile(bands, preamp);
    }, 250),
  []
);

useEffect(() => {
  debouncedApply(bands, preamp);
}, [bands, preamp]);

Batching Updates:

Send multiple changes in one IPC call instead of multiple:

// ❌ Bad: 3 IPC calls
await updatePreamp(preamp);
await updateBands(bands);
await saveSettings();

// ✅ Good: 1 IPC call
await updateSettings({ preamp, bands, autoSave: true });

Async vs Sync Commands:

  • Use async for I/O operations (file reads, network)
  • Use sync for CPU-bound operations < 16ms (audio math)
  • Never block the main thread for > 16ms

6. Security Considerations

Input Validation:

Always validate on the Rust side:

#[command]
pub fn set_frequency(band_id: usize, freq: f32) -> Result<(), String> {
    if !(20.0..=20000.0).contains(&freq) {
        return Err("Frequency must be between 20 and 20000 Hz".to_string());
    }
    if band_id >= MAX_BANDS {
        return Err(format!("Band ID {} exceeds maximum {}", band_id, MAX_BANDS));
    }
    // Safe to proceed
    Ok(())
}

Path Traversal Prevention:

use std::path::PathBuf;

#[command]
pub fn load_profile_by_path(path: String) -> Result<EqProfile, String> {
    let profile_dir = ProfileManager::get_profile_dir()?;
    let requested_path = PathBuf::from(&path);

    // Prevent path traversal attacks
    if !requested_path.starts_with(&profile_dir) {
        return Err("Invalid profile path".to_string());
    }

    ProfileManager::load_from_path(requested_path)
}

Command Registration

All commands must be registered in src-tauri/src/lib.rs:

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![
            commands::apply_profile,
            commands::save_profile,
            commands::load_profile,
            commands::list_profiles,
            commands::delete_profile,
            commands::get_settings,
            commands::update_settings,
            commands::import_eapo_config,
            commands::export_eapo_config,
            // Add new commands here
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Testing IPC Commands

Unit Tests (Rust):

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_save_profile() {
        let result = save_profile(
            "Test Profile".to_string(),
            vec![],
            0.0
        ).await;
        assert!(result.is_ok());
    }
}

Integration Tests (Frontend):

import { describe, it, expect } from 'vitest';
import { saveProfile } from './tauri';

describe('Tauri IPC', () => {
  it('should save profile', async () => {
    const result = await saveProfile('Test', [], 0);
    expect(result).toBeDefined();
  });
});

Common Pitfalls

  1. Command Name Mismatch

    • Rust: save_profile (snake_case)
    • Frontend: 'save_profile' (must match exactly)
  2. Async Overuse

    • Don't use async for simple calculations
    • Only for I/O or operations > 16ms
  3. Missing Error Handling

    • Always return Result<T, String>, never panic in commands
    • Handle all error branches
  4. Type Mismatches

    • Rust f32 vs TypeScript number (OK)
    • Rust u32 serializes as number, but may overflow in JS
    • Use i64 for large numbers (JS safe integer limit: 2^53)
  5. Serialization Failures

    • Missing #[derive(Serialize, Deserialize)]
    • Circular references (use #[serde(skip)])
    • Non-serializable types (file handles, closures)
  6. Event Memory Leaks

    • Always unlisten in useEffect cleanup
    • Remove listeners when components unmount

Reference Materials

For detailed examples and patterns, see:

  • references/command_patterns.md - Common IPC command patterns
  • references/type_mappings.md - Rust ↔ TypeScript type reference
  • references/event_patterns.md - Event-driven communication examples

Development Workflow

When implementing new IPC features:

  1. Define Rust types in src-tauri/src/types.rs
  2. Implement command in src-tauri/src/commands.rs
  3. Register command in src-tauri/src/lib.rs
  4. Mirror types in lib/types.ts
  5. Create wrapper in lib/tauri.ts
  6. Test with curl or Tauri dev tools
  7. Integrate in React components
  8. Add error handling throughout the stack

Performance Benchmarks

Target response times:

  • Simple queries (get settings): < 1ms
  • File I/O (load profile): < 10ms
  • Config write (apply EQ): < 50ms
  • Heavy computation (FFT analysis): < 100ms

If commands exceed these targets:

  • Profile with cargo flamegraph
  • Consider caching frequently accessed data
  • Move heavy work to separate threads
  • Use streaming for large data sets

Support

For Tauri-specific questions: