はじめに
こんにちは、はじめましての方ははじめまして、おやすみなさい。へおんです。
この記事で得られる情報は下記の通りです
- やること、やったことでの感想
- Python から Rust へ移行するにあたってのメリット/デメリット
- Rust へ移行するにあたっての技術情報(mongoDB,iced,OpenCV,tesseract)
やること、やったこと
目的は Python で書いたスマブラSPの収集ツールを Rust で書き直すことです。
改善したことと同時に、理由を記述していきます。
Python での GUI がいまいち決まらなかった
Python で戦歴を表示するにあたって、
ユーザーによってUIを変更できそうなライブラリを探していたんですが、
Python 系の描画 API とまだ仲良くなってないので、
いまいち融通がきかない、かつ、描画もままならない、
といった状況になってしまいました。
二兎を追う者は一兎をも得ずの典型的な例です。
ここを Rust なら低レベルな API とも仲良くなれるんじゃないかと思いました。
結果的には、GUI をデザインし直し、ユーザーでの変更可能性を捨てて、
よくありそうな WebView っぽいアプリや、配信でのコメント一覧、みたいな構造に落ち着きました!
リファクタリングをしたかった
Python を使うと誰もが同じようなコードが生成される、
とどっかで聞きましたが、
私にそれは当てはまらなかったようです、、、。
数千行のソースを書くと、
IDE 任せに、クラスにするまでもない、適当に短くしていった関数たちが見づらく、
結果的にごちゃごちゃになっていました。
いきあたりばったりで設計をしてなかったせいなので、
Rust へ移行するにあたって、
ちゃんとモジュールとクラス(Rust は struct しかないけど)を分割したので、
非常に助かりました。
使用した GUI のライブラリも細切りにするタイプを選んだので、
ユーザーの融通は利かないけど保守、改変はしやすいという結果になりました!
作ったのに使ってなかった
Python で作ったのはいいけど、思った以上に処理能力が必要だったので、起動に時間がかかってました。
なので C++ 並に早いし使いやすいと噂の Rust をついでに触ってみようと移行を決心しました。
もしかしたら pyinstaller で --onefile オプションをつけていたせいかもしれないので、
Rust では Inno Setup を使ってインストーラー形式にすることにしました。
こちらのサイトには大変お世話になっております。
Python から Rust へ移行するにあたってのメリット/デメリット
私自身過去に様々な言語を触ってきていたので、Rust 導入への抵抗は全く無かったです。
Rust くんに寄り添っている Cargo くんが有能だったり、リファレンスが豊富だったので、
環境構築やコンパイルなどもスムーズに行えて、非常に助かりました。
メリット
- Rust くんはやいはやい
cpython とはいえ、自身コンパイル言語には勝てなかったよ、、、。 - pip の代わりの crate が便利
Rust のコメント仕様がそもそもですが、docs.rs でのリファレンスが非常に助かりました。
手軽にソースやバージョンを確認出来るのも非常によかったです。 - 低レベル言語能力者に優しい
高級言語だと、困った時に詰んだり、黒魔術に手を出しがちですが、
低レベルならわかりやすく、ライブラリで済むことが多く、最悪アセンブラでゴリ押し出来るのがいいですね(白目)
デメリット
- やはり学習コストは高かったよ
C++ や Python, Java, C#, JS, VB,... と様々な言語を通ってきてるとはいえ、
ライフタイム
や借用の概念
は新しく理解する必要がありました。
ジェネリック + ライフタイム + 借用 + 無名関数 のコンボが最悪ですとても勉強になりました! - デバッグに時間がかかる
これはもう低級言語の宿命ですね。ビルドしてるので仕方ない。
とはいえ Rust くんは、手軽にキャッシュを使って依存関係をスキップできるので、遅い方の早い方といった感じです。
sccache くんに助けてもらったのですが、最終的に依存関係が子の孫の、といった感じで 400 を超えていたので非常に助かりました。
結果的に、メリットのほうが多かったような気がします。
Rust へ移行するにあたっての技術情報
様々なクレートを使わせて頂いてるので、上げだすときりがないですが、躓いたところを重点的に書いていきたいと思います。
GUI は kivy から iced へ
Rust は C++ などから移行してきて GUI 系も繁栄している印象なので、
様々なクレートが存在したのですが、
Windows ということで、
様々な描画系を使える iced をフロントに、
イベント系に強そうな winit をバックエンドに
それぞれが合体してる iced_winit を選択しました。
iced は本体に view でオブジェクトをぶら下げていく記述をする方式のクレートなので、
WPF 系から来た人は馴染みやすいと思います。
またサンプルが gif 付きでソースとの比較がしやすく、動作も確認できて非常に好印象なクレートです。
具体的には下記のようになりました。
- iced_winit::Program
- iced::pane_grid
- iced::Content(戦闘中情報)
- iced::Row とか iced::Column とかで iced::Text とか iced::Input を持つ
- iced::Content(戦闘中情報)
- iced::pane_grid
- iced::Content(戦歴)
iced::Content(戦闘情報)
iced::Content(戦闘情報2)
...
- iced::Content(戦歴)
- iced::pane_grid
iced::Content(設定)
- iced::pane_grid
PaneGrid::new が結構難関で、無名関数を渡すのですが、
FnOnce
でも FnMut
でもない Fn
なのが、
Rust 初心者が飼いならすのには非常に困難でした。
self が持ってるものを clone してローカル変数に、それを move して渡すか、
匿名構造体を通じてこっそり渡すかの選択を迫られます。
私は未だにどちらがいいのかよくわかってません。
保存形式は json から MongoDB へ
Python のほうでは、戦歴を json で保存していたのですが、
削除はかんたんでいいのですが、やはりファイルだと増えていけばいくほど読み込みが遅くなっていたので、
そこを改善しに行きました。
そして、やはり情報を保存する媒体といえばみんな大好き DB ですが、
過去に MySQL は触ったことがあったので、MongoDB を Rust で使ってみました。
使用する DB や コレクション を作成しなくても、
値を突っ込んだ時点で勝手に作成されるというのは、
非常に導入コストが低くてよいと思います!(後で気づいた)
クエリというよりは json ファイルをディレクトリ階層に追加していく感覚に近いという印象です。
挿入と参照は下記のような感じで気軽に(様々なクレート使えば)行えます。
シリアライズは別になりますが serde_json との連携が非常によいので、
いろんな記事を参考にさせていただきました!
ありがとうございます!
impl BattleHistory {
pub fn new() -> Self {
// MongoDBへの接続(の代わりに作成)とdatabaseの取得
let options = async_std::task::block_on(async move {
ClientOptions::parse("mongodb://localhost:27017/").await.unwrap()
});
let client = Client::with_options(options).unwrap();
Self {
db_client: client,
}
}
/// なにかのクエリを非同期でタイムアウト付きで実行する
pub fn do_query_with_timeout<F, T>(future: F) -> Option<T>
where
F: async_std::future::Future<Output = Result<T, mongodb::error::Error>>,
{
async_std::task::block_on(async {
let timeout = async_std::future::timeout(std::time::Duration::from_secs(5), future).await;
match timeout {
Ok(result) => match result {
Ok(result_object) => Some(result_object),
Err(e) => {
// mongodb::error
log::error!("[db_err] {:?}", e);
None
},
},
Err(e) => {
// async_std::timeout::error
log::error!("[timeout] {:?}", e);
None
}
}
})
}
/// battle_data コレクションへ戦歴情報を挿入
pub fn insert_data(&mut self, data: &SmashbrosData) -> Option<String> {
let database = self.db_client.database("smabrog-db");
let collection_ref = database.collection("battle_data_col").clone();
let serialized_data = bson::to_bson(data).unwrap();
let data_document = serialized_data.as_document().unwrap();
match Self::do_query_with_timeout(
collection_ref.insert_one( data_document.to_owned(), None )
) {
// 何故か ObjectId が再帰的に格納されている
Some(result) => Some(result.inserted_id.as_object_id().unwrap().to_hex()),
None => return None,
}
}
/// battle_data コレクションから戦歴情報を 直近10件 取得
pub fn find_data_limit_10(&self) -> Option<Vec<SmashbrosData>> {
let database = self.db_client.database("smabrog-db");
let collection_ref = database.collection("battle_data_col").clone();
/* mongodb のポインタ的なものをもらう */
let mut cursor: Cursor = match async_std::task::block_on(async {
let timeout = async_std::future::timeout(std::time::Duration::from_secs(5),
collection_ref.find(
None,
FindOptions::builder()
.sort(doc! { "_id": -1 })
.limit(10)
.build()
)
).await;
match timeout {
Ok(cursor) => match cursor {
Ok(cursor) => Ok(cursor),
Err(e) => Err(anyhow::anyhow!( format!("{:?}", e) )), // mongodb::error -> anyhow
},
Err(e) => Err(anyhow::anyhow!( format!("{:?}", e) )) // async_std::timeout::error -> anyhow
}
}) {
Ok(cursor) => cursor,
Err(e) => {
log::error!("[err] {:?}", e);
return None
},
};
// */
/* mongodb のポインタ的なものをもらう *
// 上記コードを下記に変更すると STATUS_STACK_OVERFLOW で落ちる(やってる内容は同じだし謎のバグ)
// async_std::task::block_on を読んだ時点で起こるっぽい(insert_with_2 は普通に実行できるから更に謎)
// アタッチデバッグしたら async-executor という crate の run あたりでスタックが溢れてるっぽい
let mut cursor: Cursor = match Self::do_query_with_timeout(
collection_ref.find(
None,
FindOptions::builder()
.sort(doc! { "_id": -1 })
.limit(10)
.build()
)
) {
Some(cursor) => cursor,
None => return None,
};
// */
// ポインタ的 から ドキュメントを取得して、コンテナに格納されたのを積む
use async_std::prelude::*;
let mut data_list: Vec<SmashbrosData> = Vec::new();
while let Some(document) = async_std::task::block_on(async{ cursor.next().await }) {
let data: SmashbrosData = bson::from_bson(bson::Bson::Document(document.unwrap())).unwrap();
data_list.push(data);
}
Some(data_list)
}
...
(ちゃっかり困ってるところをさらけ出していきます)
引き継いだのは OpenCV と tesseract-OCR
OpenCV と tesseract の使い方はいろんな記事があるのでそこを参照していただきまして、
とりあえず少し躓いたところがあったので列挙していきます。
-
NaN はあるのに inf を丸めるメソッドが、OpenCV にない。
真っ黒な画面なのにif 1.0 <= captured_ratio
で一致してしまうので、見つけるまで厄介でした。
utils.rs#L34 -
tesseract を使うと「Warning: Invalid resolution 0 dpi. Using 70 instead.」が出てくるのでそれを消す
当初は WinRT のほうから tesseract を使おうと思っていたのですが、
OpenCV との連携が難しいので(Buffer 系のメソッドが消えてるので Mat から変換ができず断念)、
仕方無しクレート手伝ってもらいに vcpkg 経由で拾うことになりました。環境変数が必要になってます。
export VCPKG_DEFAULT_TRIPLET=x64-windows-static export RUSTFLAGS=-Ctarget-feature=+crt-static
なのかわからないですが、前述の警告文が出てきたので、消すために追記しました。
他にも python にはあった、tesseract_layout を指定するメソッドがなかったので、フォークして追記してます(set_page_seg_mode)。
utils.rs#L111
さいごに
今回 Python から Rust へと移行し、起動するだけでよくなって、私のスマブラライフが豊かになりました!
様々なクレートや記事を公開してくださっている方々に感謝を。ありがとう!
またソースコードやリポジトリを覗いていただいて、改善点などがあれば、
プルリクエストなりここのコメントなりでご教授していただけると幸いです!
ではまたどこかで
乙ノシ