T-CREATOR

Tauri で OS ネイティブ API にアクセスする方法

Tauri で OS ネイティブ API にアクセスする方法

最近の Web 技術の進歩により、デスクトップアプリ開発の選択肢が大幅に広がっています。その中でも特に注目されているのが Tauri です。Tauri は軽量でセキュアなデスクトップアプリケーション開発フレームワークとして、多くの開発者から支持を集めています。

本記事では、Tauri でアクセスできる OS ネイティブ API を機能別に分類し、各 API の使い方と実装例を詳しく解説いたします。Web 技術の知識を活かしながら、本格的なデスクトップアプリケーションを作成したい方にとって、実践的な内容となっております。

背景

Tauri のアーキテクチャ

Tauri は現代的なデスクトップアプリ開発において、革新的なアプローチを採用しています。従来の Electron とは異なり、軽量性とセキュリティを重視した設計となっております。

以下の図は、Tauri アプリケーションの基本的なアーキテクチャを示しています。

mermaidclassDiagram
    class TauriApp {
        +Frontend: React/Vue/Svelte
        +Backend: Rust
        +Bridge: Tauri Commands
    }

    class OSAPIs {
        +FileSystem
        +Notifications
        +Clipboard
        +Dialog
        +Window
        +Shell
    }

    TauriApp --> OSAPIs : access via Commands

この図からわかるように、Tauri アプリケーションはフロントエンド(Web 技術)とバックエンド(Rust)が明確に分離されており、Tauri Commands がその橋渡しを担っています。

Frontend - Backend 分離アーキテクチャ

Tauri の最大の特徴は、フロントエンドとバックエンドの完全な分離です。フロントエンド側では慣れ親しんだ React、Vue、Svelte などのフレームワークを使用できます。一方、バックエンド側は Rust で記述され、高いパフォーマンスとメモリ安全性を提供します。

この分離により、開発者は各層の特性を最大限に活用できるのです。Web 技術の柔軟性と Rust の堅牢性を同時に享受できるのは、他のフレームワークにはない大きなメリットといえるでしょう。

セキュリティモデル

Tauri は「ゼロトラスト」の原則に基づいたセキュリティモデルを採用しています。デフォルトでは、すべての API アクセスが無効になっており、開発者が明示的に許可したもののみが使用可能です。

セキュリティ機能の詳細について、以下の図で説明いたします。

mermaidflowchart TD
    A[フロントエンド] -->|Command呼び出し| B[セキュリティレイヤー]
    B -->|許可チェック| C{allowlist確認}
    C -->|許可済み| D[Rust Command実行]
    C -->|未許可| E[エラー返却]
    D -->|結果| F[OSネイティブAPI]
    F -->|レスポンス| A
    E -->|エラー| A

この仕組みにより、意図しない API アクセスを防ぎ、アプリケーションの安全性を確保しています。

許可システム

許可システムはtauri.conf.jsonファイルで管理されます。各 API ごとに細かく設定でき、必要最小限の権限のみを付与することが可能です。これにより、セキュリティリスクを最小限に抑えながら、必要な機能を実装できます。

課題

従来の課題

Web 技術を使ったデスクトップアプリ開発には、いくつかの根本的な課題が存在していました。これらの課題を理解することで、Tauri がもたらす価値をより深く理解できるでしょう。

Web 技術だけでは実現できない機能

ブラウザベースの Web アプリケーションは、セキュリティ上の理由から多くの制限があります。ファイルシステムへの直接アクセス、システム通知の送信、クリップボードの操作など、デスクトップアプリケーションとして当然必要な機能が利用できませんでした。

従来の課題を整理すると以下のような構造になります。

mermaidstateDiagram-v2
    [*] --> WebApp
    WebApp --> 制限事項
    制限事項 --> ファイルアクセス不可
    制限事項 --> システム通知制限
    制限事項 --> ハードウェア情報取得不可
    制限事項 --> 外部プロセス実行不可
    制限事項 --> セキュリティ制約
    制限事項 --> パフォーマンス問題

これらの制限により、リッチなデスクトップ体験を提供することが困難でした。

セキュリティ制約

Web 技術の安全性は、同時に制約でもありました。Cross-Origin Resource Sharing(CORS)、Content Security Policy(CSP)などの仕組みにより、柔軟なアプリケーション開発が阻害されるケースが多々ありました。

パフォーマンスの問題

Electron などの既存ソリューションでは、Chromium エンジン全体をバンドルするため、アプリケーションサイズが大きくなり、メモリ使用量も多くなる傾向がありました。これは特に軽量なアプリケーションにとって深刻な問題でした。

Tauri でのアプローチと Mermaid 図解

Tauri はこれらの課題に対して、革新的なアプローチで解決策を提供しています。

Rust 層でのネイティブ API 呼び出し

Tauri の中核となるのは、Rust 層でのネイティブ API 呼び出し機能です。Rust の安全性とパフォーマンスを活かしながら、OS の機能に直接アクセスできます。

以下の図は、Tauri における API 呼び出しのフローを示しています。

mermaidsequenceDiagram
    participant F as Frontend
    participant TC as Tauri Command
    participant R as Rust Backend
    participant OS as OS API

    F->>TC: invoke('file_read', {path})
    TC->>R: Command実行
    R->>OS: ファイル読み込み
    OS->>R: ファイルデータ
    R->>TC: 結果返却
    TC->>F: Promise resolve

このフローにより、Web 技術の制約を超えた機能実装が可能になります。

JavaScript/TypeScript 層との連携

フロントエンド側では、Tauri API を通じて簡単にバックエンド機能を呼び出せます。TypeScript による型安全性も確保されており、開発効率が大幅に向上します。

解決策

Tauri Commands の基本

Tauri Commands は、フロントエンドとバックエンドを繋ぐ重要な仕組みです。この仕組みを理解することで、効率的なアプリケーション開発が可能になります。

Command 関数の定義

Rust 側で Command 関数を定義する基本的な方法をご紹介します。以下のコードは、シンプルなテキストファイル読み込み機能の実装例です。

rustuse tauri::command;
use std::fs;

#[command]
async fn read_text_file(path: String) -> Result<String, String> {
    match fs::read_to_string(&path) {
        Ok(content) => Ok(content),
        Err(e) => Err(format!("ファイル読み込みエラー: {}", e))
    }
}

この Command 関数では、ファイルパスを受け取り、テキストファイルの内容を返します。エラーハンドリングも適切に実装されており、安全な処理が保証されています。

次に、Command 関数を Tauri アプリケーションに登録する方法をご説明します。

rustfn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![read_text_file])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

JavaScript 側からの呼び出し

フロントエンド側では、invoke関数を使用して Command 関数を呼び出します。以下は TypeScript での実装例です。

typescriptimport { invoke } from '@tauri-apps/api/tauri';

async function readFile(filePath: string): Promise<string> {
  try {
    const content = await invoke<string>('read_text_file', {
      path: filePath,
    });
    return content;
  } catch (error) {
    console.error('ファイル読み込みに失敗しました:', error);
    throw error;
  }
}

この実装により、フロントエンド側から簡単にファイル操作が行えます。

データの受け渡し

複雑なデータ構造の受け渡しも可能です。以下は、ファイル情報を取得する Command の例です。

rustuse serde::{Deserialize, Serialize};
use std::fs::metadata;

#[derive(Serialize, Deserialize)]
struct FileInfo {
    name: String,
    size: u64,
    is_directory: bool,
    modified: u64,
}

#[command]
async fn get_file_info(path: String) -> Result<FileInfo, String> {
    match metadata(&path) {
        Ok(meta) => {
            let modified = meta.modified()
                .unwrap()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs();

            Ok(FileInfo {
                name: path.clone(),
                size: meta.len(),
                is_directory: meta.is_dir(),
                modified,
            })
        }
        Err(e) => Err(format!("ファイル情報取得エラー: {}", e))
    }
}

設定とセットアップ

tauri.conf.json の設定

Tauri アプリケーションの動作は、tauri.conf.jsonファイルで詳細に制御できます。以下は基本的な設定例です。

json{
  "build": {
    "beforeDevCommand": "yarn dev",
    "beforeBuildCommand": "yarn build",
    "devPath": "http://localhost:3000",
    "distDir": "../dist"
  },
  "package": {
    "productName": "My Tauri App",
    "version": "0.1.0"
  },
  "tauri": {
    "allowlist": {
      "all": false,
      "fs": {
        "all": false,
        "readFile": true,
        "writeFile": true,
        "createDir": true
      }
    }
  }
}

許可設定(allowlist)

セキュリティの観点から、使用する API を明示的に許可する必要があります。以下は主要な API 許可設定の例です。

json{
  "tauri": {
    "allowlist": {
      "fs": {
        "readFile": true,
        "writeFile": true,
        "readDir": true
      },
      "notification": {
        "all": true
      },
      "clipboard": {
        "all": true
      },
      "dialog": {
        "open": true,
        "save": true
      }
    }
  }
}

ビルド設定

本番環境での最適化設定も重要です。以下はパフォーマンスを重視した設定例です。

json{
  "tauri": {
    "bundle": {
      "active": true,
      "targets": "all",
      "identifier": "com.example.myapp",
      "icon": ["icons/32x32.png", "icons/128x128.png"],
      "resources": [],
      "externalBin": [],
      "copyright": "",
      "category": "DeveloperTool",
      "shortDescription": "",
      "longDescription": ""
    }
  }
}

具体例

ファイル操作 API

ファイル操作は、デスクトップアプリケーションにとって最も基本的で重要な機能の一つです。Tauri では、安全で効率的なファイル操作 API を提供しています。

ファイル読み書き

テキストファイルの基本的な読み書き操作から始めましょう。以下は、設定ファイルを管理する Command の実装例です。

rustuse tauri::{command, AppHandle};
use std::fs;
use std::path::PathBuf;

#[command]
async fn read_config_file(app: AppHandle) -> Result<String, String> {
    let app_dir = app.path_resolver()
        .app_config_dir()
        .ok_or("設定ディレクトリの取得に失敗しました")?;

    let config_path = app_dir.join("config.json");

    match fs::read_to_string(&config_path) {
        Ok(content) => Ok(content),
        Err(_) => {
            // デフォルト設定を作成
            let default_config = r#"{"theme": "light", "language": "ja"}"#;
            fs::create_dir_all(&app_dir).map_err(|e| e.to_string())?;
            fs::write(&config_path, default_config).map_err(|e| e.to_string())?;
            Ok(default_config.to_string())
        }
    }
}

ファイル書き込み機能も実装してみましょう。

rust#[command]
async fn save_config_file(app: AppHandle, content: String) -> Result<(), String> {
    let app_dir = app.path_resolver()
        .app_config_dir()
        .ok_or("設定ディレクトリの取得に失敗しました")?;

    let config_path = app_dir.join("config.json");

    fs::create_dir_all(&app_dir).map_err(|e| e.to_string())?;
    fs::write(&config_path, content).map_err(|e| e.to_string())?;

    Ok(())
}

フロントエンド側での使用例をご紹介します。

typescriptimport { invoke } from '@tauri-apps/api/tauri';

interface AppConfig {
  theme: 'light' | 'dark';
  language: string;
}

class ConfigManager {
  async loadConfig(): Promise<AppConfig> {
    try {
      const configStr = await invoke<string>(
        'read_config_file'
      );
      return JSON.parse(configStr);
    } catch (error) {
      console.error('設定読み込みエラー:', error);
      return { theme: 'light', language: 'ja' };
    }
  }

  async saveConfig(config: AppConfig): Promise<void> {
    try {
      const configStr = JSON.stringify(config, null, 2);
      await invoke('save_config_file', {
        content: configStr,
      });
    } catch (error) {
      console.error('設定保存エラー:', error);
      throw error;
    }
  }
}

ディレクトリ操作

ディレクトリの作成、削除、一覧取得などの操作も重要です。以下は、プロジェクトディレクトリを管理する Command の例です。

rustuse serde::{Deserialize, Serialize};
use std::fs::{self, DirEntry};
use std::path::Path;

#[derive(Serialize, Deserialize)]
struct FileEntry {
    name: String,
    path: String,
    is_directory: bool,
    size: u64,
}

#[command]
async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
    let dir_path = Path::new(&path);

    if !dir_path.exists() {
        return Err("指定されたディレクトリが存在しません".to_string());
    }

    if !dir_path.is_dir() {
        return Err("指定されたパスはディレクトリではありません".to_string());
    }

    let entries = fs::read_dir(dir_path)
        .map_err(|e| format!("ディレクトリ読み込みエラー: {}", e))?;

    let mut result = Vec::new();

    for entry in entries {
        let entry = entry.map_err(|e| format!("エントリ読み込みエラー: {}", e))?;
        let metadata = entry.metadata()
            .map_err(|e| format!("メタデータ取得エラー: {}", e))?;

        result.push(FileEntry {
            name: entry.file_name().to_string_lossy().to_string(),
            path: entry.path().to_string_lossy().to_string(),
            is_directory: metadata.is_dir(),
            size: metadata.len(),
        });
    }

    Ok(result)
}

ファイル選択ダイアログ

ユーザーインターフェースとしてファイル選択ダイアログも重要です。Tauri では標準的なダイアログ API を提供しています。

rustuse tauri::api::dialog::FileDialogBuilder;

#[command]
async fn select_file(app: AppHandle) -> Result<Option<String>, String> {
    let result = FileDialogBuilder::new()
        .add_filter("テキストファイル", &["txt", "md"])
        .add_filter("すべてのファイル", &["*"])
        .pick_file();

    Ok(result.map(|path| path.to_string_lossy().to_string()))
}

フロントエンド側での実装は以下のようになります。

typescriptasync function openFile(): Promise<string | null> {
  try {
    const filePath = await invoke<string | null>(
      'select_file'
    );
    if (filePath) {
      console.log('選択されたファイル:', filePath);
      return filePath;
    }
    return null;
  } catch (error) {
    console.error('ファイル選択エラー:', error);
    return null;
  }
}

システム通知

デスクトップアプリケーションにおいて、システム通知は重要なユーザー体験の要素です。Tauri では、OS ネイティブの通知システムを活用できます。

通知の表示

基本的な通知表示機能を実装してみましょう。

rustuse tauri::{command, AppHandle, Manager};
use tauri::api::notification::Notification;

#[command]
async fn show_notification(
    app: AppHandle,
    title: String,
    body: String
) -> Result<(), String> {
    Notification::new(&app.config().tauri.bundle.identifier)
        .title(&title)
        .body(&body)
        .show()
        .map_err(|e| format!("通知表示エラー: {}", e))?;

    Ok(())
}

より詳細な通知設定も可能です。以下は、進捗通知の実装例です。

rust#[derive(Serialize, Deserialize)]
struct ProgressNotification {
    title: String,
    progress: u8, // 0-100
    total_items: u32,
    completed_items: u32,
}

#[command]
async fn show_progress_notification(
    app: AppHandle,
    notification: ProgressNotification
) -> Result<(), String> {
    let body = format!(
        "進捗: {}% ({}/{})",
        notification.progress,
        notification.completed_items,
        notification.total_items
    );

    Notification::new(&app.config().tauri.bundle.identifier)
        .title(&notification.title)
        .body(&body)
        .show()
        .map_err(|e| format!("進捗通知エラー: {}", e))?;

    Ok(())
}

カスタムアイコン

通知にカスタムアイコンを設定することで、アプリケーションの識別性を高められます。

rust#[command]
async fn show_custom_notification(
    app: AppHandle,
    title: String,
    body: String,
    icon_path: Option<String>
) -> Result<(), String> {
    let mut notification = Notification::new(&app.config().tauri.bundle.identifier)
        .title(&title)
        .body(&body);

    if let Some(icon) = icon_path {
        notification = notification.icon(&icon);
    }

    notification.show()
        .map_err(|e| format!("カスタム通知エラー: {}", e))?;

    Ok(())
}

アクション付き通知

より高度な機能として、アクション付きの通知も実装できます。

typescriptimport { invoke } from '@tauri-apps/api/tauri';

class NotificationManager {
  async showTaskCompleted(taskName: string): Promise<void> {
    try {
      await invoke('show_notification', {
        title: 'タスク完了',
        body: `${taskName}が正常に完了しました`,
      });
    } catch (error) {
      console.error('通知表示エラー:', error);
    }
  }

  async showProgressUpdate(
    taskName: string,
    completed: number,
    total: number
  ): Promise<void> {
    const progress = Math.round((completed / total) * 100);

    try {
      await invoke('show_progress_notification', {
        title: taskName,
        progress,
        totalItems: total,
        completedItems: completed,
      });
    } catch (error) {
      console.error('進捗通知エラー:', error);
    }
  }
}

クリップボード操作

クリップボード操作は、ユーザビリティを向上させる重要な機能です。テキストデータの読み書きから、より複雑なデータ形式まで対応できます。

テキストの読み書き

基本的なテキストクリップボード操作を実装しましょう。

rustuse tauri::api::clipboard::Clipboard;

#[command]
async fn read_clipboard() -> Result<String, String> {
    let mut clipboard = Clipboard::new()
        .map_err(|e| format!("クリップボード初期化エラー: {}", e))?;

    clipboard.read_text()
        .map_err(|e| format!("クリップボード読み込みエラー: {}", e))
}

#[command]
async fn write_clipboard(text: String) -> Result<(), String> {
    let mut clipboard = Clipboard::new()
        .map_err(|e| format!("クリップボード初期化エラー: {}", e))?;

    clipboard.write_text(&text)
        .map_err(|e| format!("クリップボード書き込みエラー: {}", e))
}

フロントエンド側での使用例です。

typescriptclass ClipboardManager {
  async copyToClipboard(text: string): Promise<boolean> {
    try {
      await invoke('write_clipboard', { text });
      return true;
    } catch (error) {
      console.error('クリップボードコピーエラー:', error);
      return false;
    }
  }

  async pasteFromClipboard(): Promise<string | null> {
    try {
      const text = await invoke<string>('read_clipboard');
      return text;
    } catch (error) {
      console.error('クリップボード貼り付けエラー:', error);
      return null;
    }
  }

  async copyCodeSnippet(
    code: string,
    language: string
  ): Promise<void> {
    const formattedCode = `\`\`\`${language}\n${code}\n\`\`\``;
    const success = await this.copyToClipboard(
      formattedCode
    );

    if (success) {
      // 通知で成功を知らせる
      await invoke('show_notification', {
        title: 'コピー完了',
        body: `${language}コードをクリップボードにコピーしました`,
      });
    }
  }
}

画像データの扱い

より高度な機能として、画像データのクリップボード操作も実装できます。

rustuse base64::{Engine as _, engine::general_purpose};
use std::fs;

#[command]
async fn copy_image_to_clipboard(image_path: String) -> Result<(), String> {
    let image_data = fs::read(&image_path)
        .map_err(|e| format!("画像読み込みエラー: {}", e))?;

    let mut clipboard = Clipboard::new()
        .map_err(|e| format!("クリップボード初期化エラー: {}", e))?;

    // 画像をBase64エンコードしてテキストとして格納
    let encoded = general_purpose::STANDARD.encode(&image_data);
    let image_text = format!("data:image/png;base64,{}", encoded);

    clipboard.write_text(&image_text)
        .map_err(|e| format!("画像クリップボード書き込みエラー: {}", e))
}

まとめ

Tauri でのネイティブアクセスの利点

本記事では、Tauri で OS ネイティブ API にアクセスする様々な方法をご紹介いたしました。Tauri を使用することで得られる主な利点をまとめてみましょう。

軽量性と高パフォーマンス

Tauri アプリケーションは、従来の Electron アプリと比較して大幅に軽量です。システムの WebView を活用することで、アプリケーションサイズを最小限に抑えながら、高いパフォーマンスを実現しています。これにより、ユーザーにとってストレスの少ない快適な体験を提供できます。

強固なセキュリティ

ゼロトラストの原則に基づいた設計により、必要最小限の権限のみを付与することが可能です。これにより、セキュリティリスクを大幅に削減し、安全なアプリケーションを構築できます。

開発効率の向上

Web 技術の知識を活かしながら、ネイティブアプリケーションの機能を実現できるため、学習コストを抑えながら高機能なアプリケーションを開発できます。TypeScript による型安全性も確保されており、開発効率が大幅に向上します。

開発時の注意点

Tauri を使用した開発において、特に注意すべきポイントをご説明いたします。

許可設定の適切な管理

tauri.conf.jsonでの allowlist 設定は、セキュリティの要です。必要以上の権限を付与しないよう、定期的に設定を見直すことが重要です。機能追加時には、最小限の権限のみを追加するよう心がけましょう。

エラーハンドリングの重要性

ネイティブ API を使用する際は、必ず適切なエラーハンドリングを実装してください。ファイルアクセス権限、ネットワーク接続状況など、様々な要因でエラーが発生する可能性があります。

プラットフォーム固有の考慮事項

Windows、macOS、Linux それぞれで動作が異なる場合があります。クロスプラットフォーム対応を行う際は、各プラットフォームでの十分なテストが必要です。

次のステップ

Tauri での OS ネイティブ API アクセスをマスターした後は、以下のような発展的な内容に挑戦してみてください。

プラグイン開発

独自の Tauri プラグインを作成することで、より高度な機能を実装できます。コミュニティプラグインの活用も効果的です。

パフォーマンス最適化

大量のデータ処理や、リアルタイム性が求められる機能では、Rust 側での最適化が重要になります。

配布とデプロイメント

アプリケーションの配布方法、自動更新機能の実装など、実際のプロダクションで必要となる機能の学習をおすすめします。

Tauri は今後も活発に開発が続けられており、新機能の追加や改善が期待されています。公式ドキュメントやコミュニティの動向を定期的にチェックし、最新の情報をキャッチアップすることが、より良いアプリケーション開発につながるでしょう。

関連リンク