Closed30

Rustを書いてみる #2

nanasinanasi

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
nanasinanasi

crateを公開する場合の使い分け:

  • binary crate: CLIツールなど、Rust関係なく使えるやつ
  • library crate: Rustから使うこと前提のやつ

っぽい?

nanasinanasi

モジュールを使うと、メソッドや構造体などの機能を1つにまとめることができる。
また、モジュールの中にモジュールを入れることもできる。

定義にはmodキーワードを使う。

mod my_feature {
  struct Feature {} // 構造体をモジュールに追加
  mod sub_feature {} // モジュールを入れ子にする
}
nanasinanasi

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

pub struct Feature {}
nanasinanasi

modのあとにファイルパスと対応する名前を書くと、別ファイルの内容がそのモジュールになる。

parentモジュールにchildrenを追加する場合:

src/lib.rs
mod parent {
  // ...
  mod children; // src/children.rsの中身がモジュールの中身になる
}
src/children.rs
// childrenモジュールの機能を書く
// mod {}で囲う必要はない
struct Feature {}
fn feature_1 {}
nanasinanasi

特別なファイル名:

  • src/lib.rs: library crateのエントリーポイント(?)
  • src/aaa/mod.rs: モジュールのパス指定にてsrc/aaa.rsと同じように動く

ファイルパスとモジュール名が対応するのがちょっと面倒そう...

nanasinanasi

cargo add --pathを使うと、ローカルにある別のcrateを使うことができる。
--pathの後には現在のパッケージのディレクトリからの相対パスを置く。

rustディレクトリにmy_crateanother_binがあって、my_crateanother_binから参照する場合:

pwd rust/another_binにて
cargo add --path ../my_crate
nanasinanasi

自分のクレートを公開したい場合は以下の方法がある。

  • crates.ioで公開する
  • GitHubで公開して、cargo add --gitで取り込む
  • etc...

crates.ioで公開する注意点として、一度公開したパッケージは削除できない
バージョンを取り下げる(yank)ことで新しいプロジェクトがそのバージョンに依存しないようにすることはできるが、これでコードやパッケージの削除ができるわけではない。

nanasinanasi

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

nanasinanasi

clapを使った場合の一般的な設計例(ChatGPTが一般的って言ってた):

src/
├── main.rs  // エントリーポイント、clap関連
├── common.rs // 共通モジュール
├── subcommands/
│   ├── mod.rs
│   ├── show_list.rs // コマンド例
│   └── other.rs
nanasinanasi
main.rs
use clap::{Parser, Subcommand};
use ...

// main.rsのみこの書き方
// これがないと後述のsubcommandsでcommonをuseできなくなる
mod common;
// ショートカットを作る場合
use common::{APP_NAME, ...};
subcommands/show_list.rs
use crate::common::{...}

// このモジュールのメイン関数
pub fn show_list() {
  // ...
  common::... // commonモジュールの内容が使える
}
nanasinanasi
subcommands/mod.rs
// show_list
mod show_list // ファイル名と揃える
use show_list::show_list; // subcommand::show_list()で呼び出せるようになる、必須ではない

mod other;
use other::other;
nanasinanasi

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() {
  // ...
}
nanasinanasi

失敗ならpanicが出る仕様

  • assert!マクロ: 実行結果がtrueにならなければpanic
  • assert_eq!マクロ: 2つの引数の実行結果が等しくならければpanic

テストを実行する方法:

  • VSCodeのRun Testsを押す
  • cargo test
nanasinanasi
  • テストは並列に実行される
  • 副作用があるテストの場合、JavaScriptのJest等と同じように工夫が必要
nanasinanasi

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(引数)]の引数部分には関数呼び出しなどを含めることもできる。

nanasinanasi

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

nanasinanasi

本に出てきた主要なクレートまとめ:

  • chrono: 日付
  • clap: CLIツールで引数を解析できる
  • serde: シリアライズ/デシリアライズ

その他のクレート:

  • fuzzy-matcher: あいまい検索
  • serde_json: JSONをシリアライズ/デシリアライズ
  • csv: CSVを読み書き
nanasinanasi

Result型を用いたエラーハンドリングでは、Okの場合は中身を取り出し、Errの場合はその中身を返す処理が頻繁に起こるらしい。
この処理は?を使うと簡単にかける。

use std::io::File;
fn open_file() -> Result<File, std::io::Error> {
  let file = File::open("path")?; // ?を使うと戻り値がErrの場合はエラーを返せる
  file
}
nanasinanasi

エラーハンドリングに便利なクレート:

  • thiserror: カスタムエラーの定義を楽にする
  • anyhow: すべてのエラーをanyhow::Errorにすることでコンパイルエラーが消える
nanasinanasi

カスタムエラーの定義には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),
}
nanasinanasi

Rustはマルチスレッドで処理を実行できる。
そのためのAPIはstd::threadにある。

thread::spawnで新しいスレッドを作成でき、そのスレッドを操作するためのハンドルを返す。
spawnの第1引数にはそのスレッドで実行したいクロージャを入れる。
ハンドルのjoinを呼び出すと実行っぽい?

// 4並列
for _ in 0..4 {
  thread::spawn(|| /* ここに処理 */);
}
nanasinanasi

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がいる
}
nanasinanasi

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

nanasinanasi

各スレッドから安全に値を読み書きするには、ミューテックスという仕組みを使う。

RustのミューテックスはArcMutexでできる。
これを使うと同時に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());
nanasinanasi

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

nanasinanasi

追記 アンインストールまとめ

まずは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を始めるときは再インストールが必要。

nanasinanasi

なお、Rustをちょっと触りたくなっただけならプレイグランドもある。
こっちはWebサイトなのでインストール不要。手軽。

https://play.rust-lang.org/

ごめんよRust...いつかまた(プレイグラウンドに)お世話になるから...

このスクラップは2ヶ月前にクローズされました