こんにちは!
普段はクライアントエンジニアしているやっすんです!
今回はTauri(rust)と形態素解析を使って開発便利ツールを作成したのでその技術紹介(前半)です!
この記事ではTauriの解説を行います!
目次
今回やりたいこと
まず初めに今回やりたいことについてですが、ある時プランナーとこんなやりとりがありました。
私(プランナーさんスキルのデータ入力ミス多いな… 設定項目が細かすぎてミスが増えてるのかな?)
私「スキルの効果の説明文から、入力データを生成できるツールとかあれば嬉しいですか?」
プランナー「めちゃくちゃ嬉しいです…!それってスキルの文言から単語を拾ってくる感じですか?」
私「そんな感じですね。「HPの高い」とか、「敵全体」とかを拾ってマスタにコピペできる形式で出力できたらいいのかなと」
プランナー「ぜひお願いしたいです! ちょっと詳細詰めてみましょうか…」
という感じで、スキルの説明文からマスタにコピペできるデータを出力するツールを作ることになりました。
説明文の解析については、古典的な手法である形態素解析を使おうとすぐに思い至りましたが、開発環境は何にしようかなと考えていました。
既に存在しているツールはElectronで作られているものが多かったのですが、何か新しいもので作ってみたいと思いちょうどベータ版から正式版になったTauriを見つけ、Tauriで作ってみることにしました!
解析する文言と出力形式について
実際に使用しているスキルの説明文や、出力形式については使用できないので、ここでは仮の説明文と出力形式で説明します。
例えば以下のようなスキルがあった時、マスタで設定する場合にはこのようになると思います。
- 敵全体に水属性の攻撃(威力:180)を行う
- 味方全体に、防御力強化(効果:30)を行う
- 自分に、攻撃力強化(効果:200)を行い、3ターン混乱状態にする
スキルID1 | ターゲット | 属性 | 効果値 | スキルID2 | ターゲット | 属性 | 効果値 |
攻撃 | 敵全体 | 水 | 180 | – | – | – | – |
防御力強化 | 味方全体 | – | 30 | – | – | – | – |
攻撃力強化 | 自分 | – | 200 | 混乱状態付与 | 自分 | – | 3 |
このように、スキルの効果説明とマスタが対応していると形態素解析を使って、説明文からマスタに入力されているデータが推測できます。
形態素解析とフロントエンド(JavaScript)については後編の記事をご覧ください!
Tauriとは
公式リポジトリにはこのように記載されています。
原文
Tauri is a framework for building tiny, blazingly fast binaries for all major desktop platforms.
Developers can integrate any front-end framework that compiles to HTML, JS and CSS for building their user interface.
The backend of the application is a rust-sourced binary with an API that the front-end can interact with.
日本語訳
Tauriは、すべての主要なデスクトップ・プラットフォーム向けに、超小型で高速なバイナリを構築するためのフレームワークです。開発者は、HTML、JS、CSSにコンパイルできる任意のフロントエンド・フレームワークを統合して、ユーザー・インターフェースを構築できる。アプリケーションのバックエンドは、フロントエンドが相互作用できるAPIを持つRustソース・バイナリである。
というように、TauriはフロントエンドをJS・CSS、バックエンドをRustで記述する形になっています。
またElectronではバイナリにChromiumとnode.jsを含んでいるのでサイズが大きくなりがちですが、TauriではOS搭載のweb rendererを使用するのでバイナリサイズが圧倒的に小さくなるという利点もあります。
ちなみにrustを触ったのはほぼ初めてだったので、もっとよりよいな書き方があるかもしれませんがご了承ください。
導入
環境
macOS 14.6.1
@tauri-apps/cli 1.3.1 -> 1.6.2
Rust 1.71.1
node.js 16.15.0
Tauriの導入ですが、公式ページを確認する方が確実です。
私の場合はmacでyarnで導入しました。
環境に記載している通り、作成時のtauriのバージョンがかなり古いので現在(2024-10-01)の最新ver tauri-app@4.4.2, @tauri-apps/cil@1.6.2に合わせて説明します!
まずは公式ページのQuick Startにも記載されている下記のコマンドを実行してtauriプロジェクトの作成を行います。
1 |
$yarn create tauri-app |
このコマンドを実行すると、下記の選択を行う形になります。
- プロジェクト名
- Identifier
- フロントエンドで使用する言語
- TypeScript/JavaScript
- Rust
- .NET
- package manager
- yarn
- npm
- など
- UIテンプレート
- Vanilla
- Vue
- React
- など
- UI flavor
- TypeScript
- JavaScript
今回は、
フロントエンド -> TypeScript/JavaScript
package manager -> yarn
UIテンプレート -> react
UI flavor -> TypeScript
を選択しています。
生成が完了すると下記ののような出力がされて準備完了です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Template created! To get started run: cd tauri-app yarn yarn tauri android init yarn tauri ios init For Desktop development, run: yarn tauri dev For Android development, run: yarn tauri android dev For iOS development, run: yarn tauri ios dev |
試しに現状の状態で動かしてみます。
動作させるには、yarnを初期化後にyarn tauri dev
を実行です。
1 2 |
$yarn $yarn tauri dev |
このような画面のアプリが開けば成功です!
tauriではホットリロードを採用しているので、一度yarn tauri dev
を実行したら、あとはコードに変更を入れて保存するだけで自動的に立ち上がっているappに反映されます。
構成について
tauriのセットアップが完了したところで、今回のアプリの構成についてです。
tauriの構成としては、フロントエンドのts/jsで操作や入力を受け取り、バックエンドのrustで処理をして、フロントに返すというような構成が推奨されているようです。
また今回の前提として形態素解析を使いたいという関係上、どちらで形態素解析を行う方が楽かと考えました。
結果JavaScriptの方が、形態素解析の公開リポジトリも複数あり、ネットに数多くの情報があることから、ユーザからの入力および形態素解析をjs側で行なって、瀕死分解したものをrust側で処理してほしい出力形式に変形、js側に返して表示するという構成にすることにしました。
バックエンドの主な処理
ここからはフロントエンドで形態素分解が完了して、バックエンドにデータが渡されてからの処理について解説します。
tauri commandについて
tauriではフロントエンドとバックエンドのデータのやりとについて、tauri commandという仕組みを利用して行います。
使用方法は簡単で、rust側でattributeとして#[tauri::command]
を追加した関数を定義します。
あとはプロジェクト生成時に記述されるrun関数のinvoke_handler
に定義した関数名を記述することでrust側の準備はokです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } |
呼び出し元のjs側では、こちらもプロジェクト生成時に記述されれている import { invoke }
があることを確認して、await invoke("関数名", { [引数] })
でrust側の関数を呼び出すことができます!
1 2 3 4 5 |
import { invoke } from "@tauri-apps/api/core"; async function greet() { setGreetMsg(await invoke("greet", { name })); } |
これらを用いて、js側で形態素分解してできた配列をrust側に渡しています。
実際の関数は下記となっていて、引数にVec<String>
返り値にVec<Vec<String>>
を返します。
1 2 3 4 5 6 7 |
#[tauri::command] fn get_array(array: Vec<String>) -> Vec<Vec<String>> { let mut result = keyword_analysis(array); println!("\nresult {:?}", result); return result; } |
js側とrust側の引数・返り値の対応は
引数: js array
-> rustVec<String>
返り値: rust Vec<Vec<String>>
-> js string[][]
となっています。型整合についてはtauri側がいい感じに変換してくれているようで特に意識することなく実装することが可能です。
yamlについて
さて、形態素分解されたデータを受け取ることはできましたが、ここからそのデータの分析を行います。
- 敵全体に水属性の攻撃(威力:180)を行う
スキルID1 | ターゲット | 属性 | 効果値 | スキルID2 | ターゲット | 属性 | 効果値 |
攻撃 | 敵全体 | 水 | 180 | – | – | – | – |
これは例に挙げたスキルの説明文ですが威力はそのまま180を代入すればいいものの、スキルIDやターゲット、属性は本来日本語が入っているわけではなく、数値が入っているはずです。
例えば属性だと
- 火属性 -> 1
- 水属性 -> 2
- 雷属性 -> 3
のようにです。
スキル説明文を解析するにあたって、この水属性 = 2の対応表が必要です。
今回はその対応表をyamlファイルに記述して持たせることにしました。
yaml形式はkey/value形式がデフォルトであったり、配列なども記述できるため選択しました。
今回yamlをパースするライブラリはyaml_rustを利用しました。
1 2 3 4 5 6 7 8 9 10 11 12 |
use yaml_rust::{YamlLoader, YamlEmitter, Yaml}; fn load_yaml(path: &str, handle: tauri::AppHandle) -> Vec<Yaml> { let resource_path = handle.path_resolver() .resolve_resource(path).expect("file not found"); let mut file = File::open(&resource_path).unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents); let docs = YamlLoader::load_from_str(&contents).unwrap(); return docs; } |
初期化についてはこのような感じで、yamlファイルを開き、String型に格納後、YamlLoader::load_from_str(&contents).unwrap()
を呼ぶことで、展開されます。
展開された後の型はVec<Yaml>
型になるので、操作も行いやすいです。
コード中にあるtauri::AppHandle
, handle.path_resolver()
については後ほど解説します。
このようなyamlが定義されていた時の、中身の取得方法については、
1 2 3 4 5 6 |
foo: - list1 - list2 bar: - 1 - 2.0 |
1 2 3 4 |
let docs = load_yaml(&path, app.handle()); let doc = &docs[0]; let foo_vec = doc.["foo"].as_vec().unwrap(); |
のようにすることで取得することができます。
グローバル変数について
yamlをパースすることはできましたが、使用するたびに一回一回File::opneをして〜とやると、手間ですし非効率です。
そこでアプリの初期化時にyamlをパースしてその結果をグローバル変数に格納することにしました。
しかし大きな問題として、rustではグローバル変数自体はあるのですが、定数でしか初期化できないという制限があります。
そこで今回はrustでより便利なグローバル変数を使用できるようになる、once cellを使用することにしました。
使用方法としては、先頭でstatic [変数名]: OnceCell<型> = OnceCell::new();
を宣言します。
1 2 3 |
use once_cell::sync::{Lazy, OnceCell}; static YAML_DATA: OnceCell<Yaml> = OnceCell::new(); |
あとは任意のタイミングで、set関数を呼ぶだけで初期化が完了します。
1 2 3 |
// yamlファイル読み込み、vec化 let docs = load_yaml(&path, app.handle()); YAML_DATA.set(docs[0].clone()); |
これでどのタイミングでもyamlからデータを取り出すことができるようになりました!
この後の対応としては、存在しているスキルの種類やターゲット、効果値など様々なパラーターを全て記述してマスタに入力されるべき数値変換を行なっていきました…
無数に存在する説明文について場合分けや、特例対応などを行う必要があったので、実はここが一番時間かかりました…
mac, win向けのクロスコンパイル
ある程度実装が完了したので、一度ビルドしてみることにしました。
tauriのビルドは公式がgithub actions用に設定を用意してくれているので、ありがたくそれを使わせていただきましょう。
参考: https://github.com/tauri-apps/tauri-action
今回必要なのはMacとWindows用のアプリなので、プラットフォームをその2つに絞って記述します。
またボタンで実行したいので、on: [workflow_dispatch]
を記述してボタンでビルドが走るようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
name: 'build debug-tool' on: [workflow_dispatch] jobs: publish-tauri: permissions: contents: write strategy: fail-fast: false matrix: platform: [macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v3 - name: setup node uses: actions/setup-node@v3 with: node-version: 16 - name: install Rust stable uses: dtolnay/rust-toolchain@stable - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-20.04' run: | sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf - name: install frontend dependencies run: yarn install # change this to npm or pnpm depending on which one you use - uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version releaseName: 'App v__VERSION__' releaseBody: 'See the assets to download this version and install.' releaseDraft: true prerelease: false |
5分ほど待って、.appと.exeのビルドが完了しました!
ビルド時にファイルを含める
ビルドは完了して起動まではできたのですが、正常に動作しません…
どうやらyamlファイルがバイナリに含まれていないようなので、その対応を行う必要がありました。
方法については公式ドキュメントにしっかり記載されていて、
- tauri.conf.jsonに配置するファイル名を記述
- AppHandle.path().resolve関数でpathの解決を行う
の2つを行うことで、追加ファイルをrustから参照することができます。
tauri.conf.jsonには下記のように記述します。
追加ファイルはプロジェクトの任意の場所に配置できるため、そのパスを含むファイル名をresoucesに記述します。
1 2 3 4 5 6 7 8 9 |
{ "bundle": { "resources": [ "/absolute/path/to/textfile.txt", "relative/path/to/jsonfile.json", "resources/**/*" ] } } |
続いてpathの解決については、app.path().resolve関数を使用して解決します。
resolve関数はアプリの初期化時か、任意のタイミングで行うことができ、今回はアプリの初期化時に行なっています。
アプリの初期化時には、setup関数の引数appからresolve関数を呼ぶことができるのでこちらで解決します。
ちなみに下記はtauri v1の書き方になっています。
1 2 3 4 5 |
.setup(|app| { let path = "data.yaml"; let resource_path = app.path_resolver() .resolve_resource(path) .expect("file not found"); |
tauri v2では
1 2 |
.setup(|app| { let resource_path = app.path().resolve("data.yaml", BaseDirectory::Resource)?; |
のように記述するので、注意が必要です。
以上で、tauriの導入からビルドまでを一通り説明しました!
動作もElectronで作成したツールよりサクサク動きますし、想像していたよりも色々なことができそうなので、かなり良いアプリケーション開発環境だと思っています。
後編の記事では、形態素解析についてとtauriのフロントエンドについて解説します!