前々回のブログデスクトップアプリ開発ツール Tauri に触れてみたの続きです。前回のブログRustを学んでみた、Rustは超モダンなC言語 !?に書いたようにRust言語の基本を学んだので、Tauriのバックエンド(Core Process)を書いてみました。
作ったのは、Next.jsのReact Server Componentsを試してみたと同じく、ジャンケンReactアプリの対戦結果をRDB(Sqlite3)に格納するTauriアプリを作ってみました。コンピューター側の手の発生やジャンケンの勝敗判定もバックエンドで行う事にしました。画面に関しては前々回のブログと同じです。
Bing Image Creatorが生成した画像を使っています
SQLx
RustでRDBにアクセスする方法は他の言語同様、以下のようなものがあります
- RDB毎に作られたライブラリー
- 複数のRDBに接続できるライブラリー
- ORM(Object-relational mapping)
今回は簡単なDB操作だけなので、ネット上に情報が多かった2.カテゴリーのSQLxを使ってみました(RDBはSQLite3)。
SQLxの特徴はJavaScriptではお馴染みのasync/await
の非同期処理でRDBがアクセスできる事でしょうか。
また、SQLx CLIをインストールするとデータベースの作成やマイグレーションを実行できますが、今回は使わずにsqlite3コマンドで直接テーブルを作成しました。
サンプルコード
いきなりTauriバックエンドを作るのは難しいと思い。まずはRDBアクセスを行う簡単なRustのプログラムを作成しました。
Cargo.toml
外部クレート(他の言語でいる外部ライブラリー)をCargo.toml
ファイルの[dependencies]
に書いておくとcargo run
した際にインストールされます。
SQLxのInstallにはsqlxをどのように使うかにより[dependencies]
の記入方法が書かれているでコピーします。
- sqlxは非同期処理はtokioを使います、sqlite3を使うのでTLS(SSL通信)は不要です
- Rustは非同期処理
async/await
をサポートしていますがラインタイムはインストールする必要があります。ここではデファクトスタンダード的なtokioを選択しました - randは乱数のライブラリー(クレート)、C言語やJSの標準の乱数よりは安全性が高いようです
[package]
name = "rust_sqlite"
version = "0.1.0"
edition = "2021"
[dependencies]
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"
main.rs
このコードではusers
テーブルにあるデータの取得(select)、挿入(insert)を行うコードです。
- ① RDBのデータを格納する構造体
- derive マクロでデバッグ用プリントと
sqlx::FromRow
を指定しています - FromRowがないと
query_as
でのselectの戻り値をUser構造体に入れることができませんでした
- derive マクロでデバッグ用プリントと
- ②
users
テーブルの全データを取得しUser構造体の配列で戻す関数- 引数は、DB接続プール
- SQLxを使うとasync/awaitで非同期処理が行えます
- 戻り値は、正常な値とエラーの値を戻せる
Result
型です query_as
関数は実行したSQL文の結果をUser構造体(の配列)を戻しますfetch_all
メソッドで全データの取得を実行await?
メソッドはawiatした結果がエラー値ならここで実行を終了しエラー値を戻します- 正しく実行できた場合は、
Ok
バリアントで正常な値users
を戻します
- ③
users
テーブルにレコードを挿入する関数- 引数は、DB接続プールとレコードの
name
カラムの値 query
関数はSQL文を実行関数ですbind
メソッドでSQL文のプレースホルダー$1
に値を割当て、execute
メソッドでSQL文の実行します
- 引数は、DB接続プールとレコードの
- ④ Sqlite3への接続プールを取得する関数
- 引数は、Sqlite3のデータベースファイルのURL
SqlitePool::connect
がSqlite3の接続プールを取得する関数
- ⑤
#[tokio::main]
マクロで非同期処理のメインと指定しています - ⑥ DB接続プールを取得しpool変数に代入
- ⑦
"test_乱数の値"
という文字列を作成しnameに代入- C言語なら
sprintf
を使いますが、Rustではformat!
ですね
- C言語なら
- ⑧ nameカラムに
"test_乱数の値"
の値が入ったレコードを挿入 - ⑨ 全レコードの取得し
println!
でコンソールに表示 - ⑩ 非同期処理の終了、戻り値はユニット型
use sqlx::sqlite::SqlitePool;
use sqlx::{FromRow, Sqlite, Pool};
use rand::Rng;
#[derive(Debug, FromRow)] // ← ①
pub struct User { // ← ①
pub id: i64,
pub name: String,
}
// ↓ ②
async fn get_users(pool: &Pool<Sqlite>) -> Result<Vec<User>, sqlx::Error> {
let users = sqlx::query_as::<_, User>(
"SELECT id, name FROM users"
).fetch_all(pool).await?;
Ok(users)
}
// ↓ ③
async fn create_user(pool: &Pool<Sqlite>, name: &str) -> Result<(), sqlx::Error> {
sqlx::query("INSERT INTO users(name) values($1)").bind(name).execute(pool).await?;
Ok(())
}
// ↓ ④
async fn sqlite_pool(db_url: &str) -> Result<Pool<Sqlite>, sqlx::Error> {
let pool = SqlitePool::connect(db_url).await?;
Ok(pool)
}
#[tokio::main] // ← ⑤
async fn main() -> Result<(), sqlx::Error> {
let pool = sqlite_pool("sqlite:./db/database.db").await?; // ← ⑥
let mut rng = rand::thread_rng();
let name = format!("test_{}", rng.gen::<u32>()); // ← ⑦
create_user(&pool, &name).await?; // ← ⑧
let users = get_users(&pool).await?; // ← ⑨
println!("{:?}", users);
Ok(()) // ← ⑩
}
Tauriバックエンド
いよいよTauriへの組み込みですが、gihyo.jpの「軽量RustフレームワークTauriでデスクトップアプリ開発をはじめよう」のコードがとても参考になりました。ありがとうございます!
React
まずはフロントエンドのReactですが、invoke()
関数でバックエンドの関数を呼び出すだけです。
- ① 人間の手を渡し、ジャンケンを行いDBへの書き込みを行うpon関数の呼び出し
- 戻り値は、このジャンケンの結果、これをステートに追加します
- ② DBから全ジャンケンの結果を取得する、get_scores関数の呼び出し
- 戻り値は、全ジャンケンの結果(配列)、これをステートに設定
- バックエンドはIPC(プロセス間通信)で呼び出されるので、バックエンド関数の同期・非同期にかかわらず
invoke()
関数は非同期呼出しになります
JyankenBox、ScoreBoxコンポーネント等は前々回と同じです。
import { useEffect, useState } from 'react';
import "./App.css";
import { invoke } from '@tauri-apps/api/core';
const JudgmentColor = ["text-[#000]", "text-[#2979ff]", "text-[#ff1744]"];
export default function Jyanken () {
const [scores, setScores] = useState<ScoreType[]>([]);
const pon = async (human: Te) => {
const score = await invoke<ScoreType>('pon', {human: human}); // ← ①
setScores([score, ...scores]);
}
useEffect(() => {
(async() => {
const dbScores = await invoke<ScoreType[]>('get_scores', {}); // ← ②
setScores(dbScores);
})();
}, []);
return (
<div className="md:ml-8">
<h1 className="my-4 ml-4 text-3xl font-bold">じゃんけん ポン!</h1>
<div className="p-3 md:p-6 bg-white md:w-3/5">
<JyankenBox pon={pon} />
<ScoreBox scores={scores} />
</div>
</div>
);
}
Rust
バックエンドのコードは、RDB操作関数と、フロントエンドから呼び出されるコマンドハンドラー、メインコードに分かれます。
RDB操作関数
ほぼサンプルコードと同じですが違いは以下のみです
- 関数名の違い
- テーブル名とテーブルの構造の違い
- ① データの構造体
Score
は、Reactから使えるようにSerialize, Deserialize
が指定されています
-- テーブル定義
CREATE TABLE scores(
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"human" INTEGER NOT NULL,
"computer" INTEGER NOT NULL,
"judgment" INTEGER NOT NULL);
コマンドハンドラー
フロントエンドからIPC(プロセス間通信)で呼び出されます。
- ②
#[tauri::command]
マクロでTauriのコマンドハンドラーだと宣言されています- Resultのエラー値はTauriでは文字列に決まっているようです
- Rsutは広域変数は非推奨なので、DB接続プールはTauriのState Managementを使って共有しています。コマンドハンドラーは
State<>
型の引数でDB接続プールを受け取っています - ③ ここでエラー値の
sqlx::Error
から文字列に変換しています
- ④ コマンドハンドラーはスレッドセーフなコードを書く必要があり、乱数発生の2行をサンプルコードの⑧のように書くとエラーになってしまいます。調べてみたら、この書き方でスレッドセーフになります(いろいろと難しいですね😅)。
- ⑤ バックエンドのメインコードです、戻り値の型にあるdynの説明はこちら(いろいろと難しいですね😅)。
- ⑥ このmain関数はasyncにできません。非同期処理である
sqlite_pool
を実行し結果を待つためのblock_on
を使っていますblock_on
実行中は処理がブロックされてしまうようです
- ⑦ ②で説明したようにDB接続プールはTauriのState Managementで管理するので、この
setup
メソッドでマネージャーを呼び出してDB接続プールを渡しています
メインコード
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use sqlx::sqlite::SqlitePool;
use sqlx::{FromRow, Sqlite, Pool};
use serde::{Deserialize, Serialize};
use tauri::async_runtime::block_on;
use tauri::{Manager, State};
use rand::Rng;
#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct Score { // ← ①
pub id: i64,
pub human: i64,
pub computer: i64,
pub judgment: i64
}
async fn sqlite_select_scores(pool: &Pool<Sqlite>) -> Result<Vec<Score>, sqlx::Error> {
let scores = sqlx::query_as::<_, Score>(
"SELECT id, human, computer, judgment FROM scores ORDER BY id DESC"
).fetch_all(pool).await?;
Ok(scores)
}
async fn sqlite_insert_score(pool: &Pool<Sqlite>, human: i64, computer: i64, judgment: i64) -> Result<Score, sqlx::Error> {
let score = sqlx::query_as::<_, Score>(
"INSERT INTO scores(human, computer, judgment) values($1, $2, $3) returning *")
.bind(human)
.bind(computer)
.bind(judgment)
.fetch_one(pool).await?;
Ok(score)
}
async fn sqlite_pool(db_url: &str) -> Result<Pool<Sqlite>, sqlx::Error> {
let pool = SqlitePool::connect(db_url).await?;
Ok(pool)
}
#[tauri::command] // ← ↓ ②
async fn get_scores(pool: State<'_, sqlx::SqlitePool>) -> Result<Vec<Score>, String> {
let scores = sqlite_select_scores(&pool)
.await
.map_err(|e| e.to_string())?; // ← ③
Ok(scores)
}
#[tauri::command]
async fn pon(pool: State<'_, sqlx::SqlitePool>, human: i64) -> Result<Score, String> {
let computer = {
let mut rng = rand::thread_rng(); // ← ④
rng.gen_range(0..=2)
};
let judgment = (computer - human + 3) % 3;
let score = sqlite_insert_score(&pool, human, computer, judgment)
.await
.map_err(|e| e.to_string())?;
Ok(score)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] // ← ⑤
fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool = block_on(sqlite_pool("sqlite:../db/database.db"))?; // ← ⑥
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
get_scores,
pon
])
.setup(|app| { // ← ⑦
app.manage(pool);
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
Ok(())
}
Cargo.toml
参考までに
[package]
name = "tauri_jyanken"
version = "0.0.0"
description = "A Tauri Jyanken App"
authors = ["yuumi3"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "tauri2_2_lib"
crate-type = ["lib", "cdylib", "staticlib"]
[build-dependencies]
tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies]
tauri = { version = "2.0.0-rc", features = [] }
tauri-plugin-shell = "2.0.0-rc"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"
まとめ
Rust言語
前回のブログに書いたように、Rustは超モダンなC言語だと思います。
厳密な静的型付と所有権により、安全なプログラムを書く事ができます。そしてC言語とほぼ同じ速度で動作するプログラムを生成可能です。またビルド環境やライブラリーが現代的になっているのは素晴らしいですね!
ただし、文字列と数値を連結するだけで悩むのはC言語的ですね。またString
と&str
の違いなど、どのような機械語が生成されて動作しているのかイメージ出来ないと、効率の良いプログラムが書けないですね。
マクロなどまだまだ学ばないと行けない事がありますが、今後C言語レベルを必要とするプログラムを作る機会があれば、本気でRustを使って作ってみたいと思います。
Tauri
Tauriの記事を読んだ時に、TauriはElectronに変わるプラットフォームかと思ったのですが、実は違うプラットフォームですね。
両者共にフロントエンドはHTML, CSS, JavaScriptですが、バックエンドがJavaScriptかRustかの違いはかなり大きいです。
私が関わるような世界では、バックエンドもJavaScript(TypeScript)で書けるElectronの方が生産性の高さが有用だと思います。今回Rustのプログラムを書いてみてJavaScriptに比べると生産性の低さを実感しました。
ただし、すでにRsutのコード資産があり、それにWeb-UI付けたい場合や。Electronよりコンパクトで高速に動くツールを作りたい場合にはTauriは有用だと思います。