【Rust】Tauriで簡易的なアプリを作ってみる

Linux,Rust

こんにちは、しきゆらです。
ここ数回投稿しているTauriを使ったサンプルを写経しながら簡単なアプリを作ってみました。
今回は、その中で詰まった部分と解消方法を合わせてメモしておきます。

なお、写経元はこちら。

ここ最近のTauri記事はこの辺を参照。
【Rust】Tauriを使ってみる
【Rust】Tauriでフロントからバックエンドの処理を呼び出す


手元で環境を作ったときにインストールされたバージョンは以下の通り。

[package]
name = "kanban"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[build-dependencies]
tauri-build = { version = "1.4", features = [] }

[dependencies]
tauri = { version = "1.4", features = ["shell-open"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "sqlite", "migrate"] }
tokio = { version = "1.30.0", features = ["full"] }
futures = "0.3.28"
directories = "5.0.1"

[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

ただ、一点sqlxパッケージのバージョンアップで破壊的変更が入った影響で動かない部分があったので、その辺を調べつつ解消していきます。

躓いたのは「SQLiteにデータを保存する」の以下の部分。

この中で、以下のコードのexecuteの部分でエラーが出た。

sqlx::query("INSERT INTO cards (id, title, description) VALUES (?, ?, ?)")
    .bind(card.id)
    .bind(card.title)
    .bind(card.description)
    .execute(&mut tx) // ここでエラー
    .await?;

Githubにある写経元のパッケージを見ると、sqlxのバージョンが0.6.1でした。

最初に書いた通り手元では0.7.1なので、0.6~0.7の間に破壊的な変更があったとみてリリースノートを見てみるとありました。

Rustのことはよくわかりませんが、ExecutorをTransactionから削除されていました。
変更方法も記載されており、&mut transaction -> &mut *transactionとするだけとのことで、以下のように修正して事なきを得ました。

sqlx::query("INSERT INTO cards (id, title, description) VALUES (?, ?, ?)")
    .bind(card.id)
    .bind(card.title)
    .bind(card.description)
    .execute(&mut *tx) // <= ここを変更
    .await?;

これに伴い、txを渡している部分が軒並みおかしくなりました。
Rustはよくわかっていないですが、参照が悪いと踏んでその辺のRustのリファレンスを見てみました。

この中で、&を付けるとその値ではなく、値があるメモリの場所を渡すことができ、これを参照という。
参照から値を取得するには*を付けることで可能、とのこと。

次に、&mut *txは何を表しているか、を知る必要があります。
sqlxの実装を簡単に追ってみます。
まずは、txは以下のように宣言しており、コメントにも「トランザクションを開始」とあるのでトランザクションとして追ってみます。

pub(crate) async fn insert_card(pool: &SqlitePool, card: Card, pos: CardPos) -> DbResult<()> {
    // トランザクションを開始
    let mut tx = pool.begin().await.unwrap(); // <= txの宣言

    sqlx::query("INSERT INTO cards (id, title, description) VALUES (?, ?, ?)")
        .bind(card.id)
        .bind(card.title)
        .bind(card.description)
        .execute(&mut *tx) // <= 変更箇所
        .await?;

    // column_cardsテーブルにカードの位置を表す情報を挿入する
    insert_card_potition(&mut tx, pos.column_id, card.id, pos.position).await?;

    // トランザクションをコミット
    tx.commit().await?;

    Ok(())
}

トランザクションの実装はこうなっていました。

impl<'c, DB> Deref for Transaction<'c, DB>
where
    DB: Database,
{
    type Target = DB::Connection;

    #[inline]
    fn deref(&self) -> &Self::Target {
        &self.connection
    }
}

impl<'c, DB> DerefMut for Transaction<'c, DB>
where
    DB: Database,
{
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.connection
    }
}

この中にfn derefがあり、ここでTransactionの中にあるconnectionを返しているので、
&mut *txconnectionの参照を指していることになります。

では、connectionの参照を渡すように変更しましょう。
例えば、insert_card_potitionの場合。

// columns_cardsにカードの位置情報を挿入する
async fn insert_card_potition(
    tx: &mut Transaction<'_, Sqlite>,
    column_id: i64,
    card_id: i64,
    position: i64
) -> DbResult<()> {
    update_card_positions(&mut *tx, column_id, position,|pos| pos + 1).await?;

    sqlx::query("INSERT INTO columns_cards (column_id, card_id, position) VALUES (?, ?, ?)")
        .bind(column_id)
        .bind(card_id)
        .bind(position)
        .execute(&mut *tx) // ここがエラーになる
        .await?;

    Ok(())
}

ここでもinsert_cardと同じくSQLのクエリを実行しているところ。 したがってexecuteが求めているのはConnectionになります。
一方で、この関数が受け取っているのはTransactionの参照になります。 必要なのはTransactionの中にあるconnectionなので一段階ラップに包まれている状態。
なので、insert_cardよりもう1つ参照を外してあげればよいわけです。 同じ変数名なのでわかりにくいので書き換えてみます。insert_card_potitiontxTransactionの参照なのでtx_pとでもしてみます。

async fn insert_card_potition(
    tx_p: &mut Transaction<'_, Sqlite>,
    column_id: i64,
    card_id: i64,
    position: i64
) -> DbResult<()> {
    update_card_positions(&mut *tx_p, column_id, position,|pos| pos + 1).await?;

    sqlx::query("INSERT INTO columns_cards (column_id, card_id, position) VALUES (?, ?, ?)")
        .bind(column_id)
        .bind(card_id)
        .bind(position)
        .execute(&mut **tx_p) // <= 参照を2重で外す
        .await?;

    Ok(())
}

insert_card_potisionの引数tx_pTransactionへの参照なので、まずは*tx_pTransactionの参照からTransaction自体を取得。
その後、Transactionの参照外しからConnectionを取得する形です。
Transactionの参照 ⇒ *をつけて Transaction自体⇒ さらに*を付けてConnection、の順番でほしい値を取得すればよいですね。

以前もRustの所有権をふ~ん、と読み飛ばしていたので ここで改めて調べたり考えるきっかけになったので良かったといえばよかったです。

あとは、SQLのタイポがないかを確認しながら写経すれば動くはずです。

まとめ

今回は、Tauriを使ったサンプルプログラムを動かす過程でライブラリの破壊的変更に当たったので
原因を追いつつ対応してみました。
Rustの所有権よくわからんなぁと思っていましたが、今回のコードを追う中で苦手意識は少し減りました。

手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた

クジラ飛行机
3,799円(04/29 22:13時点)
発売日: 2022/01/20
Amazonの情報を掲載しています
プログラミングRust 第2版

プログラミングRust 第2版

Jim Blandy, Jason Orendorff, Leonora F. S. Tindall
5,280円(04/29 07:30時点)
Amazonの情報を掲載しています

今回は、ここまで。

おわり

Posted by しきゆら