Rustを書いてみる #2

Rustのpackageとcrateは似ているけど別物。
crateの種類:
- binary crate: 実行ファイルになるやつ。
main.rs
ファイルにfn main
関数を持つ - library crate: ライブラリ。
lib.rs
にコードを書く。通常crateと言ったらこっち
packageとはcrateをまとめたもの。
0~1つのlibrary crateと任意の数のbinary crateを持つ。
Cargo.toml
ファイルによってメタ情報を定義できる。
library crateを含むpackageは--lib
オプションで雛形が作れる:
cargo new --lib my_package

crateを公開する場合の使い分け:
- binary crate: CLIツールなど、Rust関係なく使えるやつ
- library crate: Rustから使うこと前提のやつ
っぽい?

モジュールを使うと、メソッドや構造体などの機能を1つにまとめることができる。
また、モジュールの中にモジュールを入れることもできる。
定義にはmod
キーワードを使う。
mod my_feature {
struct Feature {} // 構造体をモジュールに追加
mod sub_feature {} // モジュールを入れ子にする
}

モジュールのメンバーはデフォルトで非公開。
外部のモジュールに公開するにはpub
キーワードを使う。
pub struct Feature {}

mod
のあとにファイルパスと対応する名前を書くと、別ファイルの内容がそのモジュールになる。
parent
モジュールにchildren
を追加する場合:
mod parent {
// ...
mod children; // src/children.rsの中身がモジュールの中身になる
}
// childrenモジュールの機能を書く
// mod {}で囲う必要はない
struct Feature {}
fn feature_1 {}

特別なファイル名:
-
src/lib.rs
: library crateのエントリーポイント(?) -
src/aaa/mod.rs
: モジュールのパス指定にてsrc/aaa.rs
と同じように動く
ファイルパスとモジュール名が対応するのがちょっと面倒そう...

cargo add --path
を使うと、ローカルにある別のcrateを使うことができる。
--path
の後には現在のパッケージのディレクトリからの相対パスを置く。
rust
ディレクトリにmy_crate
とanother_bin
があって、my_crate
をanother_bin
から参照する場合:
cargo add --path ../my_crate

自分のクレートを公開したい場合は以下の方法がある。
- crates.ioで公開する
- GitHubで公開して、
cargo add --git
で取り込む - etc...
crates.ioで公開する注意点として、一度公開したパッケージは削除できない。
バージョンを取り下げる(yank
)ことで新しいプロジェクトがそのバージョンに依存しないようにすることはできるが、これでコードやパッケージの削除ができるわけではない。

この仕様があるおかげで、太古のnpmのleft-pad
事件のようなことは起こらないらしい。
クレートを使う側の安全面と作る側の自由を天秤にかけた結果、Rustは安全を取った。

clap
を使った場合の一般的な設計例(ChatGPTが一般的って言ってた):
src/
├── main.rs // エントリーポイント、clap関連
├── common.rs // 共通モジュール
├── subcommands/
│ ├── mod.rs
│ ├── show_list.rs // コマンド例
│ └── other.rs

use clap::{Parser, Subcommand};
use ...
// main.rsのみこの書き方
// これがないと後述のsubcommandsでcommonをuseできなくなる
mod common;
// ショートカットを作る場合
use common::{APP_NAME, ...};
use crate::common::{...}
// このモジュールのメイン関数
pub fn show_list() {
// ...
common::... // commonモジュールの内容が使える
}

// show_list
mod show_list // ファイル名と揃える
use show_list::show_list; // subcommand::show_list()で呼び出せるようになる、必須ではない
mod other;
use other::other;

Rustにはテスト機能が標準で備わっている。
やりかた:
// 一般的な足し算関数
fn add() { ... }
#[cfg(test)] // ここから先はテスト時のみコンパイルされる
mod tests{ // [cfg(tests)]をまとめて適用するためのモジュール
use super::*; // このファイルのすべてのやつを使う
#[test] // この関数がテストであることを示す
fn test_add_1() {
assert_eq!(add(2, 3), 5);
}
// テストはいくつも書ける
#[test]
fn test_add_1() {
// ...
}

失敗ならpanic
が出る仕様
-
assert!
マクロ: 実行結果がtrue
にならなければpanic
-
assert_eq!
マクロ: 2つの引数の実行結果が等しくならければpanic
テストを実行する方法:
- VSCodeのRun Testsを押す
cargo test

- テストは並列に実行される
- 副作用があるテストの場合、JavaScriptのJest等と同じように工夫が必要

rstest
クレート: 一つのテスト項目を複数のテストケースでやりたい場合に便利
use rstest::rstest
#[rstest]
#[case(2, 3, 5)]
#[case(3, 5, 8)]
#[case(25, 54, 79)]
fn test_add(#[case] n1, #[case] n2, #[case] sum) {
assert_eq!(add(n1, n2), sum);
}
なお#[case(引数)]
の引数部分には関数呼び出しなどを含めることもできる。

RustでデバッグするにはCodeLLDB
という拡張機能を使うといいらしい。
これを入れて、デバッグの構成のところでCodeLLDB
を選ぶとデバッガが動く。

本に出てきた主要なクレートまとめ:
-
chrono
: 日付 -
clap
: CLIツールで引数を解析できる -
serde
: シリアライズ/デシリアライズ
その他のクレート:
-
fuzzy-matcher
: あいまい検索 -
serde_json
: JSONをシリアライズ/デシリアライズ -
csv
: CSVを読み書き

Result
型を用いたエラーハンドリングでは、Ok
の場合は中身を取り出し、Err
の場合はその中身を返す処理が頻繁に起こるらしい。
この処理は?
を使うと簡単にかける。
use std::io::File;
fn open_file() -> Result<File, std::io::Error> {
let file = File::open("path")?; // ?を使うと戻り値がErrの場合はエラーを返せる
file
}

エラーハンドリングに便利なクレート:
-
thiserror
: カスタムエラーの定義を楽にする -
anyhow
: すべてのエラーをanyhow::Error
にすることでコンパイルエラーが消える

カスタムエラーの定義にはenum
が使える
thiserror
を併用する場合:
#[derive(thiserror:Error, Debug)] // Debugは必須
enum MyError {
#[error("IO ERROR! {0}")] // Displayトレイトが実装できる?
IO(#[from] std::io::Error),
#[error("JSON ERROR! {0}")]
JSON(#[from] serde_json::Error),
}

Rustはマルチスレッドで処理を実行できる。
そのためのAPIはstd::thread
にある。
thread::spawn
で新しいスレッドを作成でき、そのスレッドを操作するためのハンドルを返す。
spawn
の第1引数にはそのスレッドで実行したいクロージャを入れる。
ハンドルのjoin
を呼び出すと実行っぽい?
// 4並列
for _ in 0..4 {
thread::spawn(|| /* ここに処理 */);
}

static
はどのスレッドからでも参照できる変数。
ただし、static mut
は不可解な挙動を呼び起こす可能性が高いため、unsafe
ブロック内でしか使えない。
static mut counter: i32 = 0; // 定義にunsafeはいらない
fn main() {
for _ in 0..4 {
thread::spawn(move || {
unsafe { counter += 1 } // 書き込みにはunsafeがいる
});
}
println!("counter = {}", unsafe { counter }); // 読み取りにもunsafeがいる
}

なお、上のコードの実行結果は4だとは限らない。

各スレッドから安全に値を読み書きするには、ミューテックスという仕組みを使う。
RustのミューテックスはArc
とMutex
でできる。
これを使うと同時に1スレッドしかその値を読み書きできなくなり、スレッドセーフになる。
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let counter = counter.clone(); // thread::spawnは所有権を要求する
handles.push(thread::spawn(move || {
let mut writer = counter.lock().unwrap(); // ロックを取得する
*writer += 1; // 参照から読み書きできる
}));
}
for h in handles {
h.join().unwrap()
}
dbg!(counter.as_ref().lock().unwrap());

おわり。
Rustはマクロが強力だと思った

追記 アンインストールまとめ
まずはVSCodeの拡張機能を削除する
- rust-analyzer
- Even Better Toml: Tomlを使う機会がほかにない場合
そしたらrustup self uninstall
を実行する
これを実行するとCargoなどのツール群とrustup
そのものが消え...る前に以下のメッセージが出る
Thanks for hacking in Rust!
This will uninstall all Rust toolchains and data, and remove
$HOME/.cargo/bin from your PATH environment variable.Continue? (y/N)
y
で削除ができる。全部消えるので、再びRustを始めるときは再インストールが必要。