Lesson 14-Tauri Core Concepts

Tauri Main Process and Renderer Process

Tauri Architecture Overview

Tauri builds cross-platform applications using Rust and system-native Webview:

  • Main Process:
    • Runs Rust, handling core logic and native functionality.
    • Manages windows, system interactions, and IPC.
  • Renderer Process:
    • Runs Webview, executing frontend code (HTML/CSS/JS).
    • Communicates with the main process via Webview.
  • Communication Bridge:
    • Uses Rust’s command system and JavaScript’s IPC.

Differences and Collaboration Between Main and Renderer Processes

  • Main Process:
    • Controls the application lifecycle.
    • Handles native operations like file system and network.
  • Renderer Process:
    • Renders the UI and handles user interactions.
  • Collaboration:
    • The main process responds to frontend requests via Rust commands.
    • The renderer process invokes main process functions through window.__TAURI__.

Development Environment Setup

Initialize Project

npx create-tauri-app my-tauri-app
cd my-tauri-app
npm install

Project Structure

my-tauri-app/
├── src/                  # Frontend code
├── src-tauri/            # Rust main process code
   ├── Cargo.toml
   ├── tauri.conf.json
   └── src/
       └── main.rs
├── package.json

Tauri Command System

Command System Basics

Tauri’s command system enables the frontend to call Rust-defined functions via JavaScript:

  • Rust Side: Uses #[tauri::command] to define commands.
  • Frontend Side: Uses tauri.invoke to call commands.

Defining and Invoking Commands

Example Code

src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
src/main.js
const { invoke } = window.__TAURI__.tauri;

async function sayHello() {
    const name = document.getElementById('nameInput').value;
    const result = await invoke('greet', { name });
    document.getElementById('output').textContent = result;
}

document.getElementById('greetButton').addEventListener('click', sayHello);
src/index.html
<!DOCTYPE html>
<html>
<head>
  <title>Greet App</title>
</head>
<body>
  <input id="nameInput" placeholder="Enter your name">
  <button id="greetButton">Greet</button>
  <div id="output"></div>
  <script src="main.js"></script>
</body>
</html>

Running the Application

npm run tauri dev

Analysis

  • #[tauri::command]: Marks a Rust function as a command.
  • invoke: Frontend calls the Rust function.
  • Parameter Passing: Serialized in JSON format.

Understanding the tauri::api Module

Functionality of tauri::api Module

The tauri::api module provides built-in tools for system interactions:

  • tauri::api::dialog: Dialogs.
  • tauri::api::file: File operations.
  • tauri::api::http: Network requests.

Common API Examples

File Operations

src-tauri/src/main.rs
use tauri::api::file::write_text;

#[tauri::command]
fn save_file(content: String) -> Result<(), String> {
    write_text("output.txt", &content).map_err(|e| e.to_string())?;
    Ok(())
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![save_file])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
src-tauri/tauri.conf.json
{
  "tauri": {
    "allowlist": {
      "fs": {
        "writeFile": true
      }
    }
  }
}
src/main.js
async function saveContent() {
    const content = document.getElementById('contentInput').value;
    await invoke('save_file', { content });
    alert('Content saved!');
}

Dialogs

src-tauri/src/main.rs
use tauri::api::dialog::MessageDialogBuilder;

#[tauri::command]
fn show_dialog(message: String) {
    MessageDialogBuilder::new("Alert", message)
        .kind(tauri::api::dialog::MessageDialogKind::Info)
        .show();
}
src/main.js
async function showDialog() {
    await invoke('show_dialog', { message: 'Hello from Tauri!' });
}

Analysis

  • Permissions: Must be enabled in tauri.conf.json.
  • Invocation: Commands bridge Rust and frontend.

State Management and Message Passing

State Management in Tauri

Tauri provides State for managing shared state in the Rust main process.

Example Code

src-tauri/src/main.rs
use tauri::State;
use std::sync::Mutex;

struct AppState {
    tasks: Mutex<Vec<String>>,
}

#[tauri::command]
fn add_task(state: State<AppState>, task: String) {
    let mut tasks = state.tasks.lock().unwrap();
    tasks.push(task);
}

#[tauri::command]
fn get_tasks(state: State<AppState>) -> Vec<String> {
    let tasks = state.tasks.lock().unwrap();
    tasks.clone()
}

fn main() {
    let state = AppState {
        tasks: Mutex::new(vec![]),
    };

    tauri::Builder::default()
        .manage(state)
        .invoke_handler(tauri::generate_handler![add_task, get_tasks])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Analysis

  • State: Shared state in Rust.
  • Mutex: Thread-safe lock.

Message Passing Between Frontend and Backend

Example Code

src/main.js
const { invoke, event } = window.__TAURI__.tauri;

async function addTask() {
    const task = document.getElementById('taskInput').value;
    await invoke('add_task', { task });
    updateTasks();
}

async function updateTasks() {
    const tasks = await invoke('get_tasks');
    document.getElementById('taskList').innerHTML = tasks.map(t => `<li>${t}</li>`).join('');
}

event.listen('task-added', (event) => {
    updateTasks();
});

document.getElementById('addButton').addEventListener('click', addTask);
window.addEventListener('DOMContentLoaded', updateTasks);
src-tauri/src/main.rs
#[tauri::command]
fn add_task(state: State<AppState>, task: String, window: tauri::Window) {
    let mut tasks = state.tasks.lock().unwrap();
    tasks.push(task.clone());
    window.emit("task-added", &task).unwrap();
}

Analysis

  • invoke: Calls Rust commands.
  • event.listen: Listens for Rust events.

Plugins and Extended Functionality

Introduction to Tauri’s Plugin System

Tauri supports functionality extensions via plugins:

  • Built-in Plugins: e.g., tauri-plugin-sql.
  • Custom Plugins: Written in Rust.

Creating and Using Custom Plugins

Creating a Plugin

cargo new --lib my-plugin
cd my-plugin
my-plugin/Cargo.toml
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
tauri = "1.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
my-plugin/src/lib.rs
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Runtime, Manager};

#[tauri::command]
fn custom_command() -> String {
    "Hello from plugin!".to_string()
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("my-plugin")
        .invoke_handler(tauri::generate_handler![custom_command])
        .build()
}

Integrating the Plugin

src-tauri/Cargo.toml
[dependencies]
my-plugin = { path = "../my-plugin" }
src-tauri/src/main.rs
use my_plugin::init;

fn main() {
    tauri::Builder::default()
        .plugin(init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
src/main.js
async function callPlugin() {
    const result = await invoke('custom_command', {}, { plugin: 'my-plugin' });
    console.log(result);
}

Analysis

  • Plugin: Extends Tauri functionality.
  • Invocation: Specified via invoke with plugin namespace.

Comprehensive Case Study: Task Manager with Plugin Integration

Requirements Analysis

Build a task manager that:

  • Manages a task list in the main window.
  • Runs custom commands in a tools window.
  • Uses a plugin to log actions.

Implementation Code Breakdown

src-tauri/tauri.conf.json

{
  "tauri": {
    "allowlist": {
      "all": false
    }
  }
}

my-plugin/src/lib.rs

use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Runtime, Manager};

#[tauri::command]
fn log_message(message: String, app: tauri::AppHandle) {
    println!("Plugin Log: {}", message);
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("logger")
        .invoke_handler(tauri::generate_handler![log_message])
        .build()
}

src-tauri/Cargo.toml

[dependencies]
tauri = { version = "1.5", features = ["api-all"] }
my-plugin = { path = "../my-plugin" }

src-tauri/src/main.rs

use tauri::{State, Manager};
use std::sync::Mutex;

struct AppState {
    tasks: Mutex<Vec<String>>,
}

#[tauri::command]
fn add_task(state: State<AppState>, task: String, app: tauri::AppHandle) {
    let mut tasks = state.tasks.lock().unwrap();
    tasks.push(task.clone());
    app.emit_all("task-added", &task).unwrap();
}

#[tauri::command]
fn get_tasks(state: State<AppState>) -> Vec<String> {
    let tasks = state.tasks.lock().unwrap();
    tasks.clone()
}

fn main() {
    let state = AppState {
        tasks: Mutex::new(vec![]),
    };

    tauri::Builder::default()
        .manage(state)
        .plugin(my_plugin::init())
        .invoke_handler(tauri::generate_handler![add_task, get_tasks])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

src/main.js

const { invoke, event } = window.__TAURI__.tauri;

async function addTask() {
    const task = document.getElementById('taskInput').value;
    await invoke('add_task', { task });
    await invoke('log_message', { message: `Task added: ${task}` }, { plugin: 'logger' });
    updateTasks();
}

async function updateTasks() {
    const tasks = await invoke('get_tasks');
    document.getElementById('taskList').innerHTML = tasks.map(t => `<li>${t}</li>`).join('');
}

event.listen('task-added', updateTasks);

document.getElementById('addButton').addEventListener('click', addTask);
window.addEventListener('DOMContentLoaded', updateTasks);

src/index.html

<!DOCTYPE html>
<html>
<head>
  <title>Task Manager</title>
</head>
<body>
  <h1>Task Manager</h1>
  <input id="taskInput" placeholder="Enter task">
  <button id="addButton">Add</button>
  <ul id="taskList"></ul>
  <script src="main.js"></script>
</body>
</html>

Running the Application

npm run tauri dev

Analysis

  • Main Process: Manages task state.
  • Renderer Process: Displays tasks.
  • Plugin: Logs actions.

Advanced Techniques and Considerations

Performance Optimization Tips

  • Reduce IPC: Batch command calls.
  • State Caching: Avoid frequent queries.

Debugging Techniques

  • Rust Logging:
println!("Debug: {:?}", state);
  • Frontend Debugging: Use Webview developer tools.

Summary and Learning Resources

This tutorial covers Tauri’s main and renderer processes, command system, tauri::api, state management, and plugin development in detail. The comprehensive case study demonstrates their practical application. Mastering these skills enables you to develop efficient Tauri applications.

Share your love