5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tauriで「フロントエンドもRust(Leptos)」にしたら正直しんどかった話(数独アプリ)— Building a Tauri App with Rust-only Frontend (Leptos) Was Honestly Tough

5
Last updated at Posted at 2025-12-28

導入 / Introduction

数独というパズルがありまして、以下のようなやつですね。

  • 問題
    スクリーンショット 2025-12-21 21.12.57.png
  • 解答
    スクリーンショット 2025-12-21 21.13.12.png

これをプログラムで解いてみたいなということで、Rustで数独ソルバーをGUI付きで作ってみました。

動きは以下の様な感じです。

  • 問題を入力してSolveボタンを押下
    スクリーンショット 2025-12-21 21.12.57.png
  • 解答を表示
    スクリーンショット 2025-12-21 21.13.12.png
  • AllCrearボタンを押下 → 全マスを削除
    スクリーンショット 2025-12-21 21.16.02.png
  • 数独のルールを逸脱した状態を入力してSolveボタンを押下 → エラーダイアログを表示
    スクリーンショット 2025-12-21 21.17.31.png
  • 数独の問題として解けない状態を入力してSolveボタンを押下 → エラーダイアログを表示
    スクリーンショット 2025-12-21 21.21.45.png
    (上図の赤枠マスには6しか入り得ないため、最初から3が入力してあると解けない)

この数独ソルバーGUIのアピールポイントは何と言っても
Rust!そう、フロントエンドもバックエンドもすべてRustで作ってあるのです!
Yes, both the frontend and backend are written entirely in Rust.

RustTauriLeptosに少しでも興味のある方、または無い方(結局全員じゃねえか!)、最後までお付き合いいただけると幸いです!

(この記事は「Tauri + Rust に興味はあるが、フロントエンドまでRustにする覚悟はまだ無い人」向けです。)

数独アプリの構成 / Application Architecture

Tauriというフレームワークを使っています。
TauriはElectronを更にすごくしたやつ(雑)ということで、今注目のフレームワークです!
Tauriでアプリを作るということは、すなわち、バックエンドはRustで作るということを指します。
Tauriではフロントエンド作成時に、ReactなどのいくつかのUIテンプレートから一つを選択できるのですが、今回はフロントエンドもRustで作りたかったので、フロントエンドのUIテンプレートにはLeptosを選択しました。

アプリ全体の構成は以下の通りです。
The overall architecture of the application is shown below.

構成.drawio.png

バックエンド / Backend

フロントエンドとのやり取り / Communication Between Frontend and Backend

フロントエンドから数独の問題を受け取り、解けたら答えを、解けなかったらエラーメッセージをフロントエンドに返します。
RustにはResult型という便利なものがありまして、Tauriではこれを使ってバックエンドからフロントエンドに情報を送ることができます。

Result<Vec<Vec<i64>>, String>

バックエンドのプログラムの戻り値の型を上記のように定義することで「数独を解けたら答え(数字の配列の配列)を、解けなかったらエラーメッセージ(文字列)を返す」ことを表現できます。
このような形で結果を返すことで、フロントエンドは「答えが返ってきたけど実はこれは異常で、数字の配列の配列がNullかも知れない」のようなことで悩まずに済みます。

数独へのアプローチ / Approach to Solving Sudoku

バックエンドの役割は数独を解くことです。
今回は以下のようなアプローチで数独に取り組みました。

  • 9x9の盤面の各マスに「答えの候補数」という値を設ける
  • 問題で最初から埋まっているマスは「候補数:1(確定)」

イメージとしては下図のように、各マスに候補数(1から9のいずれか)が積み上がっている感じです。
2025-12-27 20.32.07  02c95fbf56c6.png

バックエンドのプログラムは、まず、以下の処理を繰り返します。

  • フェーズ1:全体の把握、自然に埋まるマスを埋めていく
    (1) 受け取った問題の全体を確認し「この問題だったらこのマスはこれしかない(候補数:1(確定))というマスを探し、あれば当該マスに答えを記入
    (2) 答え記入後の全体を再度確認し「この状況だったらこのマスはこれしかない(候補数:1(確定))というマスを探し、あれば当該マスに答えを記入

上記を繰り返し、状況が変わらなくなったら、次のフェーズに進みます。
次のフェーズでは以下の処理を繰り返します。

  • フェーズ2:残った候補数2以上のマスに候補を記入して解けるか試していく
    (1) 全体を確認し、候補数が最も少ないマス(2があれば2)から順に仮の答えを記入し、その状態で問題が解けるか試す
    (2) 解ければ終了。解けなければ別の答えを試す。

どうでしょうか。
これを思いついたとき「自分はなんて賢いんだろう」と思いました。
けど、これってごく普通の数独の解き方なんですかね!?
いやー自分、数独はこのプログラムでしか解いたことが無いものでして…
まあ良いや!実装行ってみましょう!

Rustで数独を解く、バックエンドのメイン部分を以下に記載します。
フロントエンドから問題を受け取り、答えを返します。

  • lib.rs
// 数独を解く
fn sudoku(board: Vec<Vec<i64>>) -> Result<Vec<Vec<i64>>, String> {
    // フロントエンドから受け取った盤面型を、盤面&各マスの候補数型に変換
    let mut sudoku_data = convert_to_candidates(&board);
 
    // 問題として成り立ってるか確認
    if !valid_sudoku(&sudoku_data){
        // 不成立問題なので終了
        return Err("unsolved".to_string());
    }

    // できるだけ絞り込む
    if !loop_narrow_down(&mut sudoku_data){
        // 最初から解けない問題だったとわかったので終了
        return Err("unsolved".to_string());        
    }

    // 絞り込み結果確認
    if all_len_one(&sudoku_data){
        // 既に答えが出ていた
        return Ok(convert_to_grid(&sudoku_data));
    }

    // 候補数が2のマスから順に試して行く
    if trial_sudoku(2, &mut sudoku_data) {
        return Ok(convert_to_grid(&sudoku_data));
    }else{
        return Err("unsolved".to_string());
    }
}

バックエンドのプログラム全文は以下にあります。
Tauri数独アプリのバックエンド

フロントエンド / Frontend

Rustで作った件について / Why I Chose to Build the Frontend in Rust

TauriのフロントエンドはReactなどを使い、TypeScriptで作ることが一般的です。
しかし、最近のTauriではフロントエンドもRustで作ることができるのです!
その選択肢として、今回はフレームワーク「Leptos」を選択しました。
Leptosは、Rust製のWebフレームワークで、フロントエンドを型安全にRustだけで記述できる点が魅力です。
Leptos is a Rust-based web framework that allows you to build type-safe frontends using only Rust.

Leptosによるフロントエンドの実装 / Frontend Implementation with Leptos

数独アプリのフロントエンドの主な役割は以下の通りです。

  • 数独の盤面を表示する
  • 数独の問題をバックエンドに送信する
  • バックエンドの戻り値に即した処理をする
    • 答えを表示
    • エラーダイアログを表示

筆者はLeptosを初めて使ったこともあり、フロントエンドの実装にはかなり苦戦しました。
作って動かすまでの苦労はReactなどより多かったと感じますが、LeptosでRustを使って作っておけば、今後の改造やメンテが楽になるかも知れないと期待しています。(※)
また、実際に計測はしていないですが、React(TypeScript)よりもLeptos(Rust)の方が速くて省メモリであることが期待できます。

(※)Rustの方がTypeScriptより型や状態管理が厳密だからシステムが大規模になってもぐちゃぐちゃになりにくい。本当か?そうであってほしい。
実際に大規模でLeptosを使っている方がいれば、ぜひ所感を教えてほしいです。

数独の盤面を表示する / Rendering the Sudoku Grid

LeptosではRustのソース内のView領域に画面表示を記載します。
以下にフロントエンドプログラム内の画面表示部分の一部抜粋を記載します。

  • sudoku_grid.rsの画面表示部分の一部抜粋
view! {
    <div
        id="sudoku-grid"
        style="
            display: grid;
            grid-template-columns: repeat(9, 60px);
            grid-template-rows: repeat(9, 60px);
        "
            >
        {(0..9).flat_map(|row| {
            (0..9).map(move |col| {
                // スタイルの設定
                let border_left = if col % 3 == 0 { "3px" } else { "1px" };
                let border_right = if col == 8 { "3px" } else { "1px" };
                let border_top = if row % 3 == 0 { "3px" } else { "1px" };
                let border_bottom = if row == 8 { "3px" } else { "1px" };
                
                let base_style = format!(
                    "width:60px;height:60px;text-align:center;font-size:30px;\
                    border-top:{} solid black;border-left:{} solid black;\
                    border-bottom:{} solid black;border-right:{} solid black;",
                    border_top, border_left, border_bottom, border_right
                );

// 省略

}

どうでしょう。何となく表を書いているっぽさは伝わりますでしょうか?
RustでHTMLを記載する的な…。
Leptosでは、このように表示もRustで行うことにより、この後出てくる数独の盤面の状態管理などもRustで行なっていくことができます。

バックエンドの戻り値に即した処理をする / Handling Backend Responses

説明の都合上「数独の問題をバックエンドに送信する」の説明は最後にして、先にこちら「バックエンドの戻り値に即した処理をする」の説明をします。

  • sudoku_grid.rsの「バックエンドの戻り値に即した処理をする」部分の抜粋
match invoke_sudoku_solver_via_tauri(board_rust).await {
    Ok(solved) => {
        // 成功処理                
        // 解を board に反映
        board.update(|b| {
            for r in 0..9 {
                for c in 0..9 {
                    if b.cells[r][c].value == 0 {
                        b.cells[r][c].value = solved[r][c];
                        b.cells[r][c].solved = true;
                    }
                }
            }
        });
        error_message.set(None); // 成功時はエラーをクリア
    }
    Err(_error_msg) => {
        let error_text = "数独の解決に失敗しました".to_string();
        // カスタムダイアログを表示
        show_dialog(&error_text).await;                        
    }
}

バックエンドを呼び出して戻り値の型によって、盤面を更新するもしくはエラーメッセージダイアログを表示しています。
ここは比較的わかりやすいと感じていただけるのではないでしょうか。
Tauriの設計思想に即した自然な流れでできていると思っています。

さて、今回の実装で最も苦労した部分が上記プログラム内の「invoke_sudoku_solver_via_tauri(board_rust)」のところでして、以降、実装部分の最後の説明としてこの話をしていきます。

数独の問題をバックエンドに送信する(フロントエンドとバックエンドのインターフェース) / Sending the Sudoku Board to the Backend — Frontend–Backend Interface in Tauri

なぜ「invoke_sudoku_solver_via_tauri(board_rust)」で苦労したのでしょうか?
RustからRustを呼び出すだけでは?
これについて、改めてTauriとは何か、というところからご説明いたします。

TauriはElectronと同様にWeb技術でUIを構築しますが、内部的には各OSのWebViewを利用しています。
そのため、Electron製アプリ(Slackなど)と同じく、UI自体はブラウザ上のWebアプリとしても成立します。

このような構成上、フロントエンドをRust(Leptos)で書いていても、WebViewの世界と接続するために最小限のJavaScriptが必要になります。
Due to this architecture, a thin layer of JavaScript is unavoidable—even in an “all-Rust” setup.

  • sudoku_grid.rsの「Javascriptの薄い層」部分の抜粋
// Rust の Vec<Vec<i64>> を JS の Array_OF_Array に変換
fn rust_board_to_js(board: &Vec<Vec<i64>>) -> Array {
	// Rust 側で JSON 文字列化
	match serde_json::to_string(board) {
		Ok(s) => {
			// JS の JSON.parse を呼んで JsValue を得る
			let parsed = match js_sys::JSON::parse(&s) {
				Ok(v) => v,
				Err(e) => {
					return Array::new();
				}
			};

			// JsValue を Array に変換して返す
			match parsed.dyn_into::<Array>() {
				Ok(arr) => {
					arr
				}
				Err(_) => {
					Array::new()
				}
			}
		}
		Err(e) => {
			Array::new()
		}
	}
}

このような層をフロントエンドとバックエンドの間に挿入し、フロントエンドとバックエンドをつなぐことにかなり苦労しました。

これでもまだ、私のTauri数独アプリは純Rust製です!と言って良いのでしょうか?
…良いでしょう!
ほら、ジュースに香料とかちょっと入っていても果汁100%って書いて良いじゃないですか!それと一緒です!(暴論)
(このあたりの「どこまでを純Rustと言っていいのか」は、正直まだ自分の中でも答えが出ていません。)

今回は「フロントエンドも全てRust製です!」と言いたかったため、以下のAI達の力を借りて、Javascriptのコード量を最小限に抑えました。

  • GitHubCopilotによるフロントエンド雛形作成
     →無料ライセンスクレジット切れのため、次項目へ
  • Codeium(GPT5.1モデル)をWindSurfエディタで使用してのリファクタリング
     →無料ライセンスクレジット切れのため、次項目へ
  • Codeium(PenguinAlphaモデル)をWindSurfエディタで使用しての未実装機能追加(エラーダイアログ実装など)

フロントエンドのプログラム全文は以下にあります。
Tauri数独アプリのフロントエンド

意外な苦労ポイント / Unexpected Pain Points

今回、思いの外苦労した点が一つありまして。
エラーダイアログ表示。これです。
スクリーンショット 2025-12-21 21.17.31.png

バックエンドのRustで数独は解けるようになったし、不慣れなLeptosでのフロントエンド実装もほぼ一通り終わったし、あとはエラー処理でもちょちょいと直して、Qiitaの執筆でも始めますか! エラーダイアログ表示なんて調べればすぐでしょ!
…そう思っていた時期が私にもありました。
ところがいざ始めてみると… エラーダイアログが出ねえ! 何だこのフォルダみたいなアイコンは!? エラーダイアログを表示するためにバックエンドに定義を書く必要がある!? それってTauri2.0では普通なの!? などと言ったドタバタ劇を前述したAI達と繰り広げる必要があり、かなり大変でした。 率直に「たかがダイアログ出すだけで何だこの苦労は…」という感想を持ちました。
To be honest, this was much harder than I initially expected.

なお、フロントエンドにTypeScript(React等)を選択していれば、ここまでハマることはなかったと思います。
この苦労の背景には、前述した「JavaScriptの薄い層」の存在があります。
Rustでフロントエンドを書いていると「RustからRustを呼び出している」感覚になりがちですが、実際には WebView 越しに JavaScript を経由して処理が行われています。
そのため、単にダイアログを表示するだけでも、想像以上に手間がかかってしまいました。

なので我が同志達、職業エンジニアの皆さん!
「ほぼほぼ終わりました。もう少しで終わりです」的な報告をするときは、くれぐれも残作業の技術的裏付けを取ってからにしましょうね!
(今回は趣味だから良かったけど、業務だったらこの難易度と作業量の見誤りは結構ヤバかった…)

同じところでハマった人がいたら、それだけでこの記事を書いた価値があったと思っています。

  • バックエンドに定義したフロントエンドに表示するためのダイアログ
// フロントエンドダイアログ表示用のコマンド
#[tauri::command]
async fn show_dialog(message: String, app: tauri::AppHandle) -> Result<(), String> {
    // Tauri 2.xでのダイアログ表示
    use tauri_plugin_dialog::{MessageDialogKind, DialogExt};
    
    app.dialog()
        .message("")  // メッセージは空にする
        .title(&message)  // タイトルに全文を表示
        .kind(MessageDialogKind::Error)
        .show(|_| {});
    
    Ok(())
}

ダイアログ表示の詳細については、前掲のフロントエンドプログラム全文とバックエンドプログラム全文をご参照ください。

まとめ、所感 / Conclusion and Reflections

Leptosを用いたRustでのTauriフロントエンド実装は難易度が高いと感じましたが、AI達の協力のおかげで勉強しながら実装、動かすところまで体験できました。
単純なTauri数独アプリを作るなら、フロントエンドにはReactなどを選択した方が情報も実績も豊富で進めやすかったかも知れませんが、今回敢えてLeptosを選択したことが、今後の役に立つことを願いたいです。

今後「Electronだと速度やメモリに限界を感じているからTauriで作りたいんだけどできる人がいなくて…」みたいな状況に巡り会えたら喜び勇んで手を挙げたいと思います。

何はともあれ「フロントエンドもRustで書きたい」という欲望を、TauriとLeptosで一度ちゃんと満たせたのは、個人的にかなり良い体験でした。

実運用や大規模開発でLeptosを使っている方がいれば、
「ここが良い」「ここが地獄だった」など、ぜひコメントで教えてください。

おまけ / Appendix

当初、バックエンドの数独を解く部分はPrologで作り、それをRustから呼び出す作りとしていました。
さすがのProlog、数独を解くのもすごく簡単に感じました。
ただ、今回はすべてRustでやってみたかったため、数独ソルバーを後からRustに置き換えました。
コメント等でご要望いただければProlog版数独ソルバーも公開したいと思います。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?