[Rust] Tauri × sidecarで他言語バイナリを連携する [Node]

[Rust] Tauri × sidecarで他言語バイナリを連携する [Node]

Clock Icon2025.05.01

Introduction

Tauriのサイドカー機能は、Tauriアプリ本体とは別に、
独立した実行可能ファイルをバンドルして連携する仕組みです。
これにより、Rust以外の言語で書かれたプログラムや既存のCLIなどを
Tauriアプリに統合できます。

サイドカーは以下のようなユースケースで使用します。

  • 既存のCLIツールをTauriのGUIから操作する
  • 他言語のエコシステムを使用したい
  • TauriのRust側で直接扱いづらい処理を実行
  • セキュリティ上分離したい処理をサイドカーで実行したい
  • パフォーマンスクリティカルな処理を実行(ex.既存C/C++最適化済みのコード)

私の場合、既存のCLIツール(node用)があり、それをTauriでラップして使いたかったので
Tauriのサイドカー機能を使いました。

Tauri?

Tauriは、Rustを使用したデスクトップアプリケーション開発のための最新のフレームワークとして注目を集めています。WebベースのUIとRustのバックエンドを組み合わせることで、軽量かつ高性能なクロスプラットフォームアプリケーションを構築できます。本レポートでは、Tauriの基本情報から開発方法まで詳しく解説します。

tauri.png

Tauriの概要と基本アーキテクチャ

Tauriは、デスクトップ/モバイルアプリを開発するためのフレームワークです。
クロスプラットフォームにも対応しており、
HTML/CSS/JavaScriptなどのWeb技術を使ってUIを構築し、
バックエンドにはRustを利用します。
(現在のバージョンは2.0)

Tauriでは以下の構成要素を組み合わせて処理します。

Webview(ウェブビュー)

アプリのフロントエンド(UI)です。
システムのWebエンジン(WindowsはWebView2、macOSはWebKitなど)を使って
フロントエンド(HTML/CSS/JS)を動かします。

Rustバックエンド

アプリのロジック部分で、Rustで実装します。
フロントエンドと通信(コマンドの呼び出しやイベントの送受信)を行います。

なお、フロント-バック間の通信はIPC(プロセス間通信)を使って
jsとRustでやりとりします。

Environment

  • MacBook Pro (14-inch, M3, 2023)
  • OS : MacOS 14.5
  • Rust : 1.86.0
  • Node : v20.19

Setup

Tauriのコマンドラインインターフェースをcargoでインストールします。

% cargo install tauri-cli

Tauriプロジェクトを簡単に作成するためのツールもインストール。

% cargo install create-tauri-app --locked

今回はcargoを使いましたが、npmからもbashからもinstallできます。

次にTauriプロジェクトを作成します。
今回はLanguageはRust、UIテンプレートはVanillaを選択しておきます。

% cargo create-tauri-app sidecar-example
% cd sidecar-example

この時点でプロジェクトフォルダ内で以下のコマンドを実行すると
アプリが起動します。

cargo tauri dev

tauri-1.png

Using Sidecar with Tauri

Tauriのサイドカー機能は、外部の実行ファイル(バイナリ)をアプリに同梱し、
アプリからそのファイルを実行できる仕組みです。
これにより、既存のCLIツールや他の言語で作られたプログラムも、
Tauriアプリの一部としてまとめて配布・利用できます。

今回は、Node.jsで作成したプログラムをpkgを使ってバイナリ化し、
サイドカーとして同梱します。
pkgは、Node.jsで書かれたアプリをNode.js本体ごと1つの実行ファイルにまとめるツールです。
そのため、Node.jsがインストールされていない環境でもアプリを配布・実行できます。

Node.jsサイドカーを作成

Tauriから実行される側の、Node用のプロジェクトを作成します。

% mkdir /path/your/node_example
% cd /path/your/node_example
% npm init -y

index.jsファイルを以下の内容で記述します。
引数があれば文字列を足して標準出力に出すだけの処理です。

///path/your/node_example/index.js

const args = process.argv.slice(2);
const inputArg = args.find(arg => arg.startsWith('--input='));

if (inputArg) {
    const input = inputArg.split('=')[1];
    console.log(`Hello, ${input}!`);
} else {
    console.log('Hello from Node.js sidecar!');
}

次にpkgをつかってNodeのバイナリを作成します。
pkgはNodeアプリを単一のバイナリ実行ファイルにパッケージングするツールです。
Nodeアプリと実行環境を単一の実行ファイルにバンドルするので、
システムにNodeがインストールされていなくても実行できます。

% npm install pkg --save

package.jsonにbuildスクリプトとpkgの設定を追加します。
npm run buildで各種プラットフォーム向けのバイナリ生成をさせてみます。

{
  ・・・・
  "scripts": {
    "build": "pkg . --targets node18-macos-arm64,node18-macos-x64,node18-linux-x64,node18-win-x64 --output dist/node-sidecar"
  },
  "pkg": {
    "assets": [],
    "targets": [
      "node18-macos-arm64",
      "node18-macos-x64",
      "node18-linux-x64",
      "node18-win-x64"
    ],
    "outputPath": "dist"
  },

  ・・・・
}

outputPathにdistを指定しているので、
バイナリは/path/your/node_example/distに出力されます。
実行してみましょう。

% npm run build

・・・

% ls node-example/dist/
node-sidecar-linux-x64	
node-sidecar-macos-arm64  
node-sidecar-macos-x64  
node-sidecar-win-x64.exe

distディレクトリに各プラットフォーム用のバイナリが生成されます。
生成された対象のバイナリを
sidecar-example/src-tauri/binariesにコピーします。

pkgの実行オプションで指定したoutputの後に続く(-macos-arm64など)のは
プラットフォームを示すサフィックスです。
外部バイナリを各サポート対象アーキテクチャで動作させるには、
指定されたパスに同じ名前とプラットフォーム毎のサフィックスを持つバイナリが必要です。

私の環境であるApple Silicon搭載のMacでは実行ファイルとして
以下のファイルが必要です。

node-sidecar-aarch64-apple-darwin

「node-sidecar」がプログラムで指定するファイル名(後述)、
「aarch64-apple-darwin」がプラットフォームのサフィックスです。
以下のコマンドで、現在のプラットフォームのサフィックスを確認できます。

% rustc -vV
・・・
host: aarch64-apple-darwin
・・・

現環境のサフィックスは「aarch64-apple-darwin」なので、
生成されたnode-sidecar-macos-arm64をリネームして
Tauri側のプロジェクトにコピーします。

#tauri側にbinariesディレクトリ作成
% mkdir -p /path/your/sidecar-example/src-tauri/binaries

% cd /path/your/node_example

# nodeバイナリをtauriアプリにコピー
% cp dist/node-sidecar-macos-arm64 /path/your/sidecar-example/src-tauri/binaries/node-sidecar-aarch64-apple-darwin

そして、Tauri側でビルド時にこのバイナリが含まれるよう、
sidecar-example/src-tauri/tauri.conf.json
tauri > bundle > externalBin にバイナリ名を追加します。

{
  ・・・
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      ・・・
    ],
    "externalBin": [
      "binaries/node-sidecar"
    ]
  }
}

ファイル名はnode-sidecar-aarch64-apple-darwinではないことに注意してください。
サフィックスは指定してはいけません。

ここの手順はドキュメントに詳細に記述してあります。

Greetボタンクリック時にサイドカーを実行

最初からあるGreetボタンをつかって、サイドカーを実行してみます。

sidecar-example/src/index.htmlのフォーム値を入力してGreetボタンを押すと、
sidecar-example/src/main.jsのgreet()関数が実行されます。
Tauriのinvoke関数でrun_sidecar(後述)を実行します。

//sidecar-example/src/main.js
const { invoke } = window.__TAURI__.core;
const { Command } = window.__TAURI__.shell;
const { listen } = window.__TAURI__.event;

let greetInputEl;
let sidecarOutputEl;

// サイドカーのイベントをListen
async function setupSidecarListeners() {
  // 標準出力のイベントをリッスン
  await listen('sidecar-output', (event) => {
    console.log('Received sidecar output:', event.payload);
    if (sidecarOutputEl) {
      sidecarOutputEl.textContent = event.payload;
    }
  });

  // エラー出力のイベントをリッスン
  await listen('sidecar-error', (event) => {
    console.error('Received sidecar error:', event.payload);
    if (sidecarOutputEl) {
      sidecarOutputEl.textContent = `Error: ${event.payload}`;
    }
  });

  // プロセス終了のイベントをリッスン
  await listen('sidecar-terminated', (event) => {
    console.log('Sidecar terminated:', event.payload);
  });
}

async function greet() {
  const name = greetInputEl.value;
  console.log('Running sidecar with name:', name);

  try {
    // サイドカーを実行
    await invoke("run_sidecar", { input: name });
  } catch (error) {
    console.error('Error running sidecar:', error);
  }
}

window.addEventListener("DOMContentLoaded", async () => {
  console.log('DOM Content Loaded');

  greetInputEl = document.querySelector("#greet-input");
  sidecarOutputEl = document.querySelector("#sidecar-output");

  // サイドカーのイベントリスナーをセットアップ
  await setupSidecarListeners();

  document.querySelector("#greet-form").addEventListener("submit", (e) => {
    e.preventDefault();
    console.log('Form submitted');
    greet();
  });
});

Rustからemitされるイベントをjs側でlisten。

次にsrc-tauri/src/lib.rsのrun_sidecar関数を実装します。
tauri_plugin_shell::ShellExtをインポートして
shell().sidecar()::AppHandle の関数を呼び出します。

use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandEvent;
use tauri::Emitter;

・・・

// サイドカーを実行する関数
#[tauri::command]
async fn run_sidecar(app: tauri::AppHandle, window: tauri::Window, input: Option<String>) -> Result<(), String> {
    println!("Starting sidecar execution...");

    // サイドカーコマンドを取得(ファイル名のみを指定)
    let sidecar_command = match app.shell().sidecar("node-sidecar") {
      ・・・
    };

    // 引数を設定
    let mut args = vec!["--app=tauri-app".to_string()];

    // 入力引数を準備
    if let Some(input_data) = input {
        println!("Adding input argument: {}", input_data);
        args.push(format!("--input={}", input_data));
    }

    println!("Executing sidecar with args: {:?}", args);

    // サイドカーを実行し、出力を取得
    let (mut rx, _child) = sidecar_command
        .args(args)
        .spawn()
        .map_err(|e| e.to_string())?;

    println!("Sidecar process started");

    // 非同期タスクとして実行
    tauri::async_runtime::spawn(async move {
        while let Some(event) = rx.recv().await {
            match event {
                CommandEvent::Stdout(line) => {
                    let line_str = String::from_utf8_lossy(&line);
                    window.emit("sidecar-output", Some(line_str.to_string()))
                        .expect("failed to emit event");
                }
                ・・・・
                CommandEvent::Terminated(status) => {
                    println!("Process terminated with status: {:?}", status);
                    window.emit("sidecar-terminated", Some(format!("{:?}", status)))
                        .expect("failed to emit event");
                    break;
                }
                _ => {}
            }
        }
    });

    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![greet, run_sidecar])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

サイドカーの出力はCommandEventとして受け取り、フロントエンドにイベントとして送信されます。
フロントエンドではsidecar-outputとsidecar-terminatedイベントをListenしているので、
結果を処理できます。

サイドカーの実行時にnode-example/index.jsが実行され、
メッセージが返されます。
その後、サイドカーの出力がRustバックエンドを通じてフロントエンドに送信されます。
フロントエンドのsetupSidecarListeners関数で出力を受け取り、sidecarOutputElに表示します。

アプリを起動し、フォームに文字列(例: Hoge)を入力してボタンを押すと、
Node.jsサイドカーの出力が画面に表示されます。

tauri.png

Summary

このように、Tauriではサイドカー機能を使って別言語のモジュールを簡単にバンドルすることができます。
既存のCLIツールや他言語で実装されたバイナリも、
Tauriアプリの一部としてシームレスに連携できるので便利です。
用途に応じていろいろな選択肢を選べることもTauriの大きな魅力だと思われます。

References

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.