Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rustでクッキング

この Rust Cookbook は、Rustエコシステムのクレートを使って、 一般的なプログラミングタスクを達成するための良い実践を示す シンプルな例を集めたものです。

本書の読み方、例の使い方、慣例に関する注意事項を含む Rust Cookbook の詳細を参照してください。

コントリビューション

このプロジェクトは、新しいRustプログラマーでも 貢献しやすく、Rustコミュニティに参加しやすいものに することを意図しています。支援を必要としており、歓迎しています。詳細は CONTRIBUTING.md を参照してください。

アルゴリズム

コマンドライン

圧縮

並行処理

暗号

データ構造

データベース

日付と時刻

開発ツール

デバッグ

バージョニング

ビルド時

エンコーディング

ファイルシステム

ハードウェアサポート

レシピクレートカテゴリ
論理 CPU コア数を確認するnum_cpus-badgecat-hardware-support-badge

メモリ管理

ネットワーキング

レシピクレートカテゴリー
未使用のポートで TCP/IP を待ち受けるstd-badgecat-net-badge

オペレーティングシステム

科学

science/mathematics

テキスト処理

Webプログラミング

Webページのスクレイピング

Uniform Resource Location(URL)

メディアタイプ(MIME)

クライアント

Web認証

レシピクレートカテゴリ
Basic認証reqwest-badgecat-net-badge

フルスタックWeb

“Cookin’ with Rust” について

目次

本書の対象読者

このクックブックは、新しい Rust プログラマーが Rust のクレート エコシステムの機能を素早く概観できるようにすることを目的として います。また、経験豊富な Rust プログラマーにとっても、 一般的なタスクをどう実現するかを手軽に思い出せるレシピ集として 役立つことを意図しています。

本書の読み方

クックブックの index には、レシピの完全な一覧が 「basics」「encoding」「concurrency」などの複数のセクションに 整理されて収められています。セクション自体もおおむね順を追って 並んでおり、後ろのセクションほど高度になり、場合によっては 前のセクションの概念を土台にしています。

インデックスでは、各セクションにレシピの一覧があります。レシピは 「範囲内の乱数を生成する」のように、達成したいタスクを簡潔に表した ものです。また各レシピには、rand-badge のように使用している クレート を示すバッジと、cat-science-badge のように それらのクレートが crates.io 上で属しているカテゴリを示すバッジが 付いています。

新しい Rust プログラマーは、最初のセクションから最後のセクションまで 無理なく読み進められるはずで、そうすることでクレートエコシステムの 全体像をしっかり把握できます。セクションの見出しをインデックス上、 またはサイドバー上でクリックすると、そのセクションのページへ移動 できます。

単純なタスクの解決方法だけを探している場合、現時点ではこの クックブックは少したどりにくくなっています。特定のレシピを見つける 最も簡単な方法は、興味のあるクレートやカテゴリを手がかりに インデックスを見ていくことです。そこからレシピ名をクリックして 内容を表示してください。この点は今後改善される予定です。

レシピの使い方

レシピは、動作するコードへすぐにアクセスできるように設計されており、 それが何をしているのかの完全な説明と、さらに詳しい情報への案内も あわせて提供します。

クックブック内のすべてのレシピは完全で自己完結したプログラムなので、 試しながら使うためにそのまま自分のプロジェクトへコピーできます。 そのためには、以下の手順に従ってください。

「一定の範囲内でランダムな数値を生成する」例を見てみましょう:

rand-badge cat-science-badge

use rand::RngExt;

fn main() {
    let mut rng = rand::rng();
    let random_number: u32 = rng.random();
    println!("Random number: {random_number}");
}

これをローカルで試すには、次のコマンドを実行して新しい cargo プロジェクトを作成し、そのディレクトリに移動します:

cargo new my-example --bin
cd my-example

次に、クレートのバッジが示すとおり、必要なクレートを Cargo.toml に 追加する必要があります。この場合は「rand」だけです。そのために、 まずインストールが必要な cargo-edit クレートが提供する cargo add コマンドを使います:

cargo install cargo-edit
cargo add rand

これで、src/main.rs を例の完全な内容で置き換えて実行できます:

cargo run

例に付随するクレートバッジは、docs.rs にある各クレートの完全な ドキュメントへリンクしており、目的に合ったクレートを選んだあとに 次に読むべきドキュメントであることがよくあります。

エラーハンドリングについて

Rust には、例外を処理するために実装される std::error::Trait があります。 このクックブックでは、例のエラーハンドリングを簡潔にするために anyhow を 使用しており、容易なエラー伝播とコンテキスト付与を提供します。ライブラリ作者 向けには、thiserror が derive マクロを用いてカスタムエラー型を作成する より構造化されたアプローチを提供します。

このクックブックでは以前 error-chain クレートを使用していましたが、 現在ではアプリケーションレベルのエラーハンドリングに推奨される手法となったため、 anyhow を使うように更新されました。Rust におけるエラーハンドリングの背景に ついてさらに知りたい場合は、Rust 本のこのページこのブログ記事 を読んでください。

クレートの掲載について

このクックブックは最終的には Rust クレートエコシステムを幅広く 網羅することを目指していますが、現時点では立ち上げと見せ方の改善を 進めている段階のため、扱う範囲は限定されています。小さな範囲から 始めて徐々に拡大していくことで、このクックブックがより早く 高品質なリソースとなり、成長しても一貫した品質水準を維持できるように なることを期待しています。

現時点でこのクックブックが焦点を当てているのは、標準ライブラリと、 「中核的」あるいは「基盤的」なクレート、つまり最も一般的な プログラミング作業を構成し、エコシステムの残りの部分がその上に 築かれているクレートです。

このクックブックは Rust Libz Blitz と密接に結びついています。 これはそのようなクレートを特定し、その品質を向上させるための プロジェクトであり、そのためクレートの選定は主にそのプロジェクトに 委ねられています。そのプロセスの一環としてすでに評価済みのクレートは クックブックの対象であり、評価待ちのクレートも同様に対象です。

アルゴリズム

ランダム値を生成する

乱数を生成する

rand-badge cat-science-badge

rand::rng() を介して取得した乱数生成器 rand::Rng を使って乱数を生成します。各スレッドには初期化済みの生成器があります。整数はその型の範囲にわたって一様に分布し、浮動小数点数は 0 以上 1 未満で一様に分布します。

use rand::RngExt;

fn main() {
    let mut rng = rand::rng();
    let random_number: u32 = rng.random();
    println!("Random number: {random_number}");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_random_number() {
        let mut rng = rand::rng();
        let _: u32 = rng.random();
    }
}

範囲内の乱数を生成する

rand-badge cat-science-badge

Rng::random_range を使って、半開区間 [0, 10)10 は含まない)内のランダムな値を生成します。

use rand::RngExt;

fn main() {
    let mut rng = rand::rng();
    println!("Integer: {}", rng.random_range(0..10));
    println!("Float: {}", rng.random_range(0.0..10.0));
}

Uniformuniform distribution に従う値を取得できます。これも同じ効果がありますが、同じ範囲で繰り返し数値を生成する場合は、より高速になることがあります。

use rand_distr::{Distribution, Uniform};

fn main() {
    let mut rng = rand::rng();
    let die =
        Uniform::new_inclusive(1, 6).expect("Failed to create uniform distribution: invalid range");

    loop {
        let throw = die.sample(&mut rng);
        println!("Roll the die: {}", throw);
        if throw == 6 {
            break;
        }
    }
}

指定した分布に従う乱数を生成する

rand_distr-badge cat-science-badge

デフォルトでは、rand クレートの乱数は 一様分布 に従います。rand_distr クレートは ほかの種類の分布を提供します。これらを使うには、分布をインスタンス化し、その後、 乱数生成器 rand::Rng を使って Distribution::sample によりその分布から サンプリングします。

利用可能な分布のドキュメントはこちらです。Normal 分布を使う例を 以下に示します。

use rand_distr::{Distribution, LogNormal, Normal};

fn main() {
    let mut rng = rand::rng();
    let normal = Normal::new(2.0, 3.0).expect("Failed to create normal distribution");
    let log_normal = LogNormal::new(1.0, 0.5).expect("Failed to create log-normal distribution");

    let v = normal.sample(&mut rng);
    println!("{} is from a N(2, 9) distribution", v);
    let v = log_normal.sample(&mut rng);
    println!("{} is from an ln N(1, 0.25) distribution", v);
}

カスタム型のランダムな値を生成する

rand-badge cat-science-badge

タプル (i32, bool, f64) と、ユーザー定義型 Point の変数をランダムに生成します。ランダム生成を可能にするために、StandardUniform に対して型 PointDistribution トレイトを実装します。

#![allow(dead_code)]
use rand::{Rng, RngExt};

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

impl Point {
    fn random<R: Rng>(rng: &mut R) -> Self {
        Point {
            x: rng.random(),
            y: rng.random(),
        }
    }
}

fn main() {
    let mut rng = rand::rng();
    let rand_tuple = rng.random::<(i32, bool, f64)>();
    let rand_point = Point::random(&mut rng);
    println!("Random tuple: {:?}", rand_tuple);
    println!("Random Point: {:?}", rand_point);
}

英数字の文字セットからランダムなパスワードを作成する

rand-badge cat-os-badge

Alphanumeric サンプルを使用して、A-Z, a-z, 0-9 の範囲にある ASCII 文字から、指定した長さの文字列をランダムに生成します。

use rand::{distr::Alphanumeric, RngExt};

fn main() {
    let password = generate_password();
    println!("{password}");
}

fn generate_password() -> String {
    const PASSWORD_LEN: usize = 30;
    let mut rng = rand::rng();

    (0..PASSWORD_LEN)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect()
}

ユーザー定義の文字セットからランダムなパスワードを作成する

rand-badge cat-os-badge

ユーザー定義のカスタムバイト文字列から、指定した長さの ASCII 文字列を random_range を使用してランダムに生成します。

use rand::RngExt;

const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
                        abcdefghijklmnopqrstuvwxyz\
                        0123456789)(*&^%$#@!~";
const PASSWORD_LEN: usize = 30;

fn main() {
    let mut rng = rand::rng();

    let password: String = (0..PASSWORD_LEN)
        .map(|_| {
            let idx = rng.random_range(0..CHARSET.len());
            char::from(CHARSET[idx])
        })
        .collect();

    println!("{password}");
}

ベクタのソート

整数のベクタをソートする

std-badge cat-science-badge

この例では、vec::sort を使って整数のベクタをソートします。代わりに vec::sort_unstable を使うこともでき、こちらのほうが高速な場合がありますが、 等しい要素の順序は保持されません。

fn main() {
    let mut vec = vec![1, 5, 10, 2, 15];
    
    vec.sort();

    assert_eq!(vec, vec![1, 2, 5, 10, 15]);
}

浮動小数点数のベクタをソートする

std-badge cat-science-badge

f32 または f64 の Vector は、vec::sort_byf64::total_cmp を使ってソートできます。 PartialOrd::partial_cmp とは異なり、total_cmp は NaN 値をパニックさせることなく処理し、 それらをソート順の末尾に配置します。

fn main() {
    let mut vec = vec![1.1_f64, 1.15, 5.5, 1.123, 2.0];

    vec.sort_by(|a, b| a.total_cmp(b));

    assert_eq!(vec, vec![1.1, 1.123, 1.15, 2.0, 5.5]);
}

構造体のベクタをソートする

std-badge cat-science-badge

nameage というプロパティを持つ Person 構造体のベクタを、その自然な 順序(名前と年齢)でソートします。Person をソート可能にするには、4 つのトレイト Eq, PartialEq, Ord, PartialOrd が必要です。これらのトレイトは単純に derive できます。 また、vec:sort_by メソッドを使ってカスタムの比較関数を提供し、年齢だけでソートすることもできます。

#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
struct Person {
    name: String,
    age: u32
}

impl Person {
    pub fn new(name: &str, age: u32) -> Self {
        Person {
            name: name.to_string(),
            age
        }
    }
}

fn main() {
    let mut people = vec![
        Person::new("Zoe", 25),
        Person::new("Al", 60),
        Person::new("John", 1),
    ];

    // 導出された自然順序(名前と年齢)で people をソート
    people.sort();

    assert_eq!(
        people,
        vec![
            Person::new("Al", 60),
            Person::new("John", 1),
            Person::new("Zoe", 25),
        ]);

    // 年齢で people をソート
    people.sort_by(|a, b| b.age.cmp(&a.age));

    assert_eq!(
        people,
        vec![
            Person::new("Al", 60),
            Person::new("Zoe", 25),
            Person::new("John", 1),
        ]);

}

非同期

はじめに

プログラムが、たとえばファイルの読み込み、サーバーからのデータ取得、タイマーの待機など、時間のかかる何かを要求するとき、選択肢は 2 つあります。何もせずに座って待つか、待っている間に別のことをするかです。

Blocking は、座って待つことを意味します。結果が返ってくるまで、プログラムは停止します。 その間は、ほかに何も起こりません。

Non-blocking (async) は、プログラムがタスクを開始し、その場を離れ、結果の準備ができたら戻ってきて受け取ることを意味します。待っている間、ほかの作業を進めることができます。

Rust では、async コードは 2 つのキーワードを使って記述します。

  • async は、関数をノンブロッキングとして示します。これを呼び出しても、すぐには実行されません。代わりに、実行の準備ができたタスクが返されます。
  • .await は、制御を戻して「このタスクを実行し、完了したら戻ってきてください」と伝える場所です。

Async Runtime

ノンブロッキングコードを実行する前に、それを管理し、どのタスクを実行するか、 いつ実行するか、そして待機中に何をするかを決定する何かが必要です。その管理役はランタイムと呼ばれます。

これをコールセンターのコーディネーターのようなものだと考えてください。1 人の担当者が 折り返しの電話を待って手持ち無沙汰になる代わりに、コーディネーターは、ほかの担当者が待っている間に新しい仕事を割り当てることで、全員が忙しく動けるようにします。

Tokio は Rust におけるそのランタイムです。これがなければ、async 関数と .await はそれ自体では何もしません。

マクロ

tokio-badge

Tokio ランタイムを開始する最も簡単な方法は、tokio::main マクロを使うことです。これを main 関数の上に置くと、ランタイムの起動、コードの実行、そして完了時のシャットダウンまで、 すべてを処理してくれます。また、main を非ブロッキングにできるため、その中で .await を直接 使用できます。

async fn fetch_network_request() -> u32 {
    89
}

#[tokio::main]
async fn main() {
    let result = fetch_network_request().await;
    assert_eq!(result, 89);
}

macrosrt-multi-thread 機能を有効にして、tokioCargo.toml に追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "rt-multi-thread"] }

Builder アプローチ

tokio-badge std-badge

tokio::main はほとんどのプログラムでうまく機能しますが、設定に関するすべての判断をそれに任せることになります。 ランタイムをどのようにセットアップするかを自分で制御したい場合は、代わりに Builder を使用してください。

Builder はレシピのようなものだと考えてください。呼び出す各メソッドが、「使用するスレッド数」「それらの名前」「各スレッドに割り当てるメモリ量」といった指示を追加します。最後に .build() を呼び出すまで、実際には何も起こりません。.build() を呼び出した時点でランタイムが作成され、実行できる状態になります。

この例では、そのレシピは 4 つのワーカースレッドを設定し、それらに “thread-one” という名前を付け、各スレッドのスタックサイズを 3MB に設定しています。

use std::io;
use tokio::runtime::Builder;

async fn fetch_network_request() -> u32 {
    89
}

fn main() -> io::Result<()> {
     let runtime = Builder::new_multi_thread()
        .worker_threads(4)
        .thread_name("thread-one")
        .thread_stack_size(3 * 1024 * 1024)
        .build()?;

    runtime.spawn(async {
        let result = fetch_network_request().await;
        assert_eq!(result, 89);
    });

    Ok(())
}

rt-multi-thread 機能を有効にして、tokioCargo.toml に追加してください。

[dependencies]
tokio = { version = "*", features = ["rt-multi-thread"] }

ファイルI/O

ファイルとディレクトリの作成

tokio-badge std-badge

ディスク上にファイルやディレクトリを作成するには時間がかかります。Tokio はこれらの 操作のノンブロッキング版を提供するため、処理が完了するまでプログラムが停止して待機する必要はありません。

File::create は新しいファイルを作成します。ファイルがすでに存在する場合は、上書きされます。 create_dir は単一のディレクトリを作成します。すでに存在する場合は失敗します。 create_dir_all はディレクトリと、そのパス上で不足している親ディレクトリを作成します。

use std::io;
use tokio::fs::File;

#[tokio::main]
async fn main() -> io::Result<()> {
    // ファイルを作成する
    File::create("data.txt").await?;

    // 単一のディレクトリを作成する
    tokio::fs::create_dir("my_dir").await?;

    // ディレクトリと不足している親ディレクトリを作成する
    tokio::fs::create_dir_all("my_dir/sub_dir/inner").await?;

    Ok(())
}

macros および fs 機能を有効にして、tokioCargo.toml に追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "fs"] }

ファイルを読み込む

tokio-badge std-badge

ディスクからファイルを読み込むには時間がかかります。Tokio はファイル読み込みのノンブロッキング版を提供しているため、プログラムはデータが返ってくるのを待つ間もほかの処理を続けられます。

  • read はファイルを生のバイト列として読み込みます。データを直接処理する必要がある場合に便利です。
  • read_to_string はファイルをプレーンテキストとして読み込みます。ファイルに可読な文字が含まれていることが分かっている場合に便利です。
use std::io;

fn process_data(data: &[u8]) {
    println!("Data Length: {}", data.len());
}

#[tokio::main]
async fn main() -> io::Result<()> {
    // Vec<u8> に読み込む
    let content = tokio::fs::read("data.txt").await?;
    process_data(&content);

    // String に読み込む
    let str_content = tokio::fs::read_to_string("data.txt").await?;
    process_data(str_content.as_bytes());

    Ok(())
}

macrosfs 機能を有効にして、Cargo.tomltokio を追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "fs"] }

ファイルの書き込み

tokio-badge std-badge

ディスクへの書き込みには時間がかかります。tokio::fs::write は、ファイルにバイト列を書き込むためのノンブロッキングな方法です。 ファイルが存在しない場合は作成し、存在する場合は上書きします。

use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    let content = b"Generic data from program!";
    tokio::fs::write("data.txt", content).await?;

    Ok(())
}

macros および fs 機能を有効にして、Cargo.tomltokio を追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "fs"] }

ファイルとディレクトリの削除

tokio-badge std-badge

ディスクからの削除には時間がかかります。Tokio はこれらの操作のノンブロッキング版を提供しているため、処理が完了するまでプログラムが停止して待機する必要はありません。

  • remove_file は単一のファイルを削除します。
  • remove_dir は単一のディレクトリを削除します。ディレクトリが空の場合にのみ動作します。
  • remove_dir_all はディレクトリとその中身をすべて削除します。
use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    // ファイルを削除する
    tokio::fs::remove_file("data.txt").await?;

    // 空のディレクトリを削除する
    tokio::fs::remove_dir("my_dir").await?;

    // ディレクトリとその内容をすべて削除する
    tokio::fs::remove_dir_all("my_dir").await?;

    Ok(())
}

macros および fs 機能を有効にして、Cargo.tomltokio を追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "fs"] }

AsyncRead と AsyncWrite

tokio-badge std-badge

AsyncReadAsyncWrite は、Tokio におけるすべてのノンブロッキング I/O の基盤です。
ノンブロッキングで読み取りまたは書き込みができる任意の型、たとえば filenetwork streambuffer は、これらのいずれか一方または両方を実装しています。

これらの拡張である AsyncReadExtAsyncWriteExt は、その上に便利なメソッドを追加します。

  • write_all は writer にバイトを書き込みます。
  • read_to_end は reader からすべてを読み取り、buffer に格納します。
  • copy は reader から writer へ直接データをストリームします。
use std::io;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    // AsyncWrite: ファイルにバイトを書き込む
    let mut file = File::create("data.txt").await?;
    file.write_all(b"Generated Data!").await?;

    // AsyncRead: ファイルからバイトを読み取る
    let mut file = File::open("data.txt").await?;
    let mut contents = Vec::new();
    file.read_to_end(&mut contents).await?;
    println!("Data Length: {}", contents.len());

    // reader から writer へストリームする
    let mut reader = File::open("data.txt").await?;
    let mut writer = File::create("copy.txt").await?;
    tokio::io::copy(&mut reader, &mut writer).await?;

    Ok(())
}

macrosio-utilfs 機能を有効にして、Cargo.tomltokio を追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "fs", "io-util"] }

タイムアウト

tokio-badge std-badge

タスクの完了に時間がかかりすぎて、いつまでも待ちたくないことがあります。timeout はタスクに時間制限を設けます。制限時間内に完了すれば結果を取得でき、そうでなければキャンセルされて、エラーが返されます。

この例では、ネットワークリクエストに完了まで 5 ミリ秒が与えられます。制限時間内に完了した場合は、結果が表示されます。時間を超過した場合は、プログラムがタイムアウトを報告します。

use std::time::Duration;
use tokio::time::timeout;

async fn fetch_network_request() -> u32 {
    89
}

#[tokio::main]
async fn main() {
    match timeout(Duration::from_millis(5), fetch_network_request()).await {
        Ok(x) => println!("Received {x}"),
        Err(_) => eprintln!("Timed Out!"),
    }
}

macrostime 機能を有効にして、tokioCargo.toml に追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "time"] }

メッセージパッシング

プログラムで複数のタスクが同時に実行されているとき、それらはときどき互いにやり取りする必要が あります。チャネルはそのための仕組みで、あるタスクがメッセージを送り、別のタスクがそれを受け取ります。 パイプのようなものだと考えてください。片方の端に何かを入れると、もう片方の端から出てきます。

有界チャネル

tokio-badge std-badge

bounded channel には、一度に保持できるメッセージ数の上限があります。チャネルがいっぱいになると、 送信側は、別のメッセージを送信できるだけの空きができるまで待機しなければなりません。

これは現実の郵便受けのようなものだと考えてください。固定数の手紙しか入れられません。いっぱいの場合、 郵便配達員は、誰かが中身を取り出して空けるまで、次の手紙を投函するのを待たなければなりません。

この例では、2 つの書店が容量 5 のチャネルを通して本を送信します。 本棚は、送られてくるものを何でも受け取ります。

use std::io;
use tokio::sync::mpsc::channel;

struct Book {
    title: &'static str,
}

impl Book {
    fn new(title: &'static str) -> Self {
        Self { title }
    }
}

#[tokio::main]
async fn main() -> io::Result<()> {
    let (book_sender, mut book_receiver) = channel(5);

    // 各書店は、それぞれ送信側のコピーを受け取る。
    let store_one_book_sender = book_sender.clone();
    let book_store_one = tokio::task::spawn(async move {
        if let Err(err) = store_one_book_sender
            .send(Book::new("Shawshank Redemption"))
            .await
        {
            eprintln!("Failed to send book from store one: {}", err);
        }
    });

    let book_store_two = tokio::task::spawn(async move {
        if let Err(err) = book_sender.send(Book::new("Secret Recipe")).await {
            eprintln!("Failed to send book from store two: {}", err);
        }
    });

    let mut shelf: Vec<Book> = Vec::new();
    // 両方の書店が送信を終えるまで、届いた本をすべて集める。
    while let Some(new_book) = book_receiver.recv().await {
        shelf.push(new_book);
    }

    book_store_one.await?;
    book_store_two.await?;

    for book in &shelf {
        println!("Title: {}", book.title);
    }

    Ok(())
}

Cargo.tomltokio を追加し、macrossync 機能を有効にしてください。

[dependencies]
tokio = { version = "*", features = ["macros", "sync"] }

無制限チャネル

tokio-badge std-badge

unbounded channel には、保持できるメッセージ数の上限がありません。送信側は待つ必要がなく、 すでにどれだけメッセージがたまっていても、いつでもメッセージを送り込めます。

デジタルな受信箱のようなものだと考えてください。新しいメッセージが届くたびに、 それは増え続けます。上限はありませんが、 メッセージが読み出されるよりも速いペースでたまっていくと、プログラムはますます多くのメモリを使用します。 受信側がまだ開いている限り、無制限チャネルへの送信は常に成功します。 受信側が遅い場合、メッセージは単にキューにたまり、待機します。

この例では、2 人がチャネルを通じてメッセージを送信します。受信箱が、届いたものを 集めます。

use std::io;
use tokio::sync::mpsc::unbounded_channel;

struct Message {
    from: &'static str,
    text: &'static str,
}

impl Message {
    fn new(from: &'static str, text: &'static str) -> Self {
        Self { from, text }
    }
}

#[tokio::main]
async fn main() -> io::Result<()> {
    let (message_sender, mut message_receiver) = unbounded_channel();

    let alice_message_sender = message_sender.clone();
    let person_one = tokio::task::spawn(async move {
        if let Err(err) = alice_message_sender.send(Message::new("Alice", "Meeting postponed")) {
            eprintln!("Failed to send message from Alice: {}", err);
        }
    });

    let person_two = tokio::task::spawn(async move {
        if let Err(err) = message_sender.send(Message::new("Bob", "Secret Leaked")) {
            eprintln!("Failed to send message from Bob: {}", err);
        }
    });

    let mut inbox: Vec<Message> = Vec::new();
    while let Some(new_book) = message_receiver.recv().await {
        inbox.push(new_book);
    }

    person_one.await?;
    person_two.await?;

    for msg in &inbox {
        println!("{} says: {}", msg.from, msg.text);
    }

    Ok(())
}

Cargo.tomltokio を追加し、macrossync feature を有効にします。

[dependencies]
tokio = { version = "*", features = ["macros", "sync"] }

最初に完了したものを選択

Ctrl-C

tokio-badge

ときどき、タスクを実行したい一方で、ユーザーが Ctrl-C を押したらすべてを停止できるようにしておきたいことがあります。 tokio::select! を使うと、まさにそれを実現できます。これは複数の処理を同時に実行し、 最初の 1 つが完了した時点で停止して、他のすべてをキャンセルします。

この例では、プログラムは何らかのデータを取得しようとします。取得が完了する前にユーザーが Ctrl-C を押した場合、 プログラムは待ち続ける代わりにクリーンにシャットダウンします。

両方の分岐は同時に開始されます。先に完了した方が選ばれ、もう一方はキャンセルされます。 fetch_data が先に完了した場合は、その結果が表示され、ループが再び実行されます。 Ctrl-C が先に来た場合は、プログラムは終了します。

use tokio::signal;
use tokio::time::{Duration, sleep};

async fn fetch_data() -> String {
    // 遅いネットワークリクエストをシミュレートする
    sleep(Duration::from_secs(5)).await;
    "data".to_string()
}

#[tokio::main]
async fn main() {
    loop {
        tokio::select! {
            result = fetch_data() => {
                println!("Got: {}", result);
            }
            _ = signal::ctrl_c() => {
                println!("Shutting down...");
                return;
            }
        }
    }
}

macrossignal 機能を有効にして、tokioCargo.toml に追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "signal"] }

構造化並行性

プログラムが複数のタスクを同時に実行する場合、それらを追跡し、 いつ終了したかを把握し、結果を集め、何か問題が起きたときにクリーンアップする方法が必要です。 このセクションでは、その方法を示します。

JoinSet

tokio-badge

JoinSet は、同時に実行されるノンブロッキングタスクのグループを保持するコンテナです。 これにタスクを追加し、その後、それらが1つずつ終了するのを待つことができます。タスクが終了したら、 その結果を取得できます。すべてのタスクが完了する前に JoinSet を破棄すると、 未完了のタスクはすべて自動的にキャンセルされます。

この例では、5つのジョブが作成されて JoinSet に追加されます。各ジョブはそれぞれ単独で作業を行い、 プログラムはそのすべてが完了するまで待ってから先へ進みます。

use tokio::task::JoinSet;

struct Job {
    id: usize,
    data: String,
}

impl Job {
    fn new(id: usize) -> Self {
        let data = "d".repeat(id);
        Self { id, data }
    }

    async fn process(&mut self) {
        // ネットワーク経由でさらにデータを取得するなど、何らかの処理を行う
        self.data = self.data.replace("d", "x");
        println!("Id {} -> {}", self.id, self.data);
    }
}

#[tokio::main]
async fn main() {
    let mut set = JoinSet::new();
    const JOB_COUNT: usize = 5;
    let jobs = (0..JOB_COUNT).map(Job::new).collect::<Vec<Job>>();

    for mut job in jobs {
        set.spawn(async move { job.process().await });
    }

    let mut complete = 0usize;
    while set.join_next().await.is_some() { // 各ジョブの終了を待つ
        complete += 1;
    }

    assert_eq!(complete, JOB_COUNT);
}

macros および rt 機能を有効にして、Cargo.tomltokio を追加してください。

[dependencies]
tokio = { version = "*", features = ["macros", "rt"] }

コマンドライン

Clap の基本

コマンドライン引数を解析する

clap-badge cat-command-line-badge

このアプリケーションは、コマンドラインインターフェイスの構造を clap のビルダースタイルを使って記述します。documentation では、 アプリケーションを生成するための他の 2 つの方法も紹介されています。

ビルダースタイルでは、それぞれの可能な引数を Arg 構造体で記述します。Arg::new() に渡される文字列は、その引数の内部 名です。short オプションと long オプションは、ユーザーが入力することになる フラグを制御します。短いフラグは -f のようになり、長い フラグは --file のようになります。

引数の値を取得するには get_one() メソッドを使用します。 このメソッドは、引数がユーザーによって指定されていた場合は Some(&value) を返し、そうでない場合は None を返します。

PathBuf を使うのは、Linux や MacOS では有効でも Rust の UTF-8 文字列では有効ではないファイルパスを扱えるようにするためです。これは ベストプラクティスです。実際にはこのようなパスに遭遇することはかなりまれですが、 これがない状態でそうしたケースに出会うと 本当に厄介です。

use std::path::PathBuf;

use clap::{Arg, Command, builder::PathBufValueParser};

fn main() {
    let matches = Command::new("My Test Program")
        .version("0.1.0")
        .about("Teaches argument parsing")
        .arg(Arg::new("file")
                 .short('f')
                 .long("file")
                 .help("A cool file")
                 .value_parser(PathBufValueParser::default()))
        .arg(Arg::new("num")
                 .short('n')
                 .long("number")
                 .help("Five less than your favorite number"))
        .get_matches();

    let default_file = PathBuf::from("input.txt");
    let myfile: &PathBuf = matches.get_one("file").unwrap_or(&default_file);
    println!("The file passed is: {}", myfile.display());

    let num_str: Option<&String> = matches.get_one("num");
    match num_str {
        None => println!("No idea what your favorite number is."),
        Some(s) => {
            let parsed: Result<i32, _> = s.parse();
            match parsed {
                Ok(n) => println!("Your favorite number must be {}.", n + 5),
                Err(_) => println!("That's not a number! {}", s),
            }
        }
    }
}

使用方法の情報は clap -h によって生成されます。サンプル アプリケーションの使用方法は次のようになります。

Teaches argument parsing

Usage: clap-cookbook [OPTIONS]

Options:
  -f, --file <file>   A cool file
  -n, --number <num>  Five less than your favorite number
  -h, --help          Print help
  -V, --version       Print version

次のようなコマンドを実行して、アプリケーションをテストできます。

$ cargo run -- -f myfile.txt -n 251

出力は次のとおりです。

The file passed is: myfile.txt
Your favorite number must be 256.

ANSI ターミナル

ANSI ターミナル

ansi_term-badge cat-command-line-badge

このプログラムは、ansi_term クレートの使用方法と、ANSI ターミナル上で青の太字テキストや黄色の下線付きテキストのような色や書式を制御するためにそれがどのように使われるかを示します。

ansi_term には 2 つの主要なデータ構造があります: ANSIStringStyle です。Style は、色、テキストを太字にするか、点滅させるか、といったスタイル情報を保持します。さらに、単純な前景色のスタイルを表す Colour バリアントもあります。ANSIStringStyle と組みになった文字列です。

注: イギリス英語では Color ではなく Colour を使うので、混同しないでください

ターミナルに色付きテキストを出力する

use ansi_term::Colour;

fn main() {
    println!("This is {} in color, {} in color and {} in color",
             Colour::Red.paint("red"),
             Colour::Blue.paint("blue"),
             Colour::Green.paint("green"));
}

ターミナルで太字テキスト

単純な前景色の変更よりも複雑なことを行うには、コードで Style 構造体を構築する必要があります。Style::new() はこの構造体を作成し、 プロパティをチェーンして指定できます。

use ansi_term::Style;

fn main() {
    println!("{} and this is not",
             Style::new().bold().paint("This is Bold"));
}

ターミナルで太字かつ色付きのテキスト

ColourStyle と同様の関数を多数実装しており、メソッドをチェーンできます。

use ansi_term::Colour;
use ansi_term::Style;

fn main(){
    println!("{}, {} and {}",
             Colour::Yellow.paint("This is colored"),
             Style::new().bold().paint("this is bold"),
             Colour::Yellow.bold().paint("this is bold and colored"));
}

圧縮

Tarball の操作

tarball を展開する

flate2-badge tar-badge cat-compression-badge

圧縮 tarball を解凍し(GzDecoder)、 現在の作業ディレクトリにある archive.tar.gz という名前の すべてのファイルを展開します(Archive::unpack)。 展開先は同じ場所です。

use std::fs::File;
use flate2::read::GzDecoder;
use tar::Archive;

fn main() -> Result<(), std::io::Error> {
    let path = "archive.tar.gz";

    let tar_gz = File::open(path)?;
    let tar = GzDecoder::new(tar_gz);
    let mut archive = Archive::new(tar);
    archive.unpack(".")?;

    Ok(())
}

ディレクトリを tarball に圧縮する

flate2-badge tar-badge cat-compression-badge

/var/log ディレクトリを archive.tar.gz に圧縮します。

GzEncoder および tar::Builder でラップされた File を作成します。

Builder::append_dir_all を使用して、/var/log ディレクトリの内容を backup/logs パスの下に再帰的にアーカイブへ追加します。 GzEncoder は、データを archive.tar.gz に書き込む前に透過的に圧縮する役割を担います。

use std::fs::File;
use flate2::Compression;
use flate2::write::GzEncoder;

fn main() -> Result<(), std::io::Error> {
    let tar_gz = File::create("archive.tar.gz")?;
    let enc = GzEncoder::new(tar_gz, Compression::default());
    let mut tar = tar::Builder::new(enc);
    tar.append_dir_all("backup/logs", "/var/log")?;
    tar.finish()?;
    Ok(())
}

内容の名前を変更せずに追加するには、Builder::append_dir_all の第 1 引数に空文字列を使用できます:

use std::fs::File;
use flate2::Compression;
use flate2::write::GzEncoder;

fn main() -> Result<(), std::io::Error> {
    let tar_gz = File::create("archive.tar.gz")?;
    let enc = GzEncoder::new(tar_gz, Compression::default());
    let mut tar = tar::Builder::new(enc);
    tar.append_dir_all("", "/var/log")?;
    tar.finish()?;
    Ok(())
}

tar::Builder のデフォルトの挙動は、GNU tar ユーティリティのデフォルト tar(1) とは異なります。 特に、tar::Builder::follow_symlinks(true)tar --dereference と同等です。

パスからプレフィックスを削除しながら tarball を展開する

flate2-badge tar-badge cat-compression-badge

Archive::entries を反復処理します。 Path::strip_prefix を使って、 指定したパスプレフィックス(bundle/logs)を削除します。最後に、 tar::EntryEntry::unpack で展開します。

use anyhow::Result;
use std::fs::File;
use std::path::PathBuf;
use flate2::read::GzDecoder;
use tar::Archive;

fn main() -> Result<()> {
    let file = File::open("archive.tar.gz")?;
    let mut archive = Archive::new(GzDecoder::new(file));
    let prefix = "bundle/logs";

    println!("Extracted the following files:");
    archive
        .entries()?
        .filter_map(|e| e.ok())
        .map(|mut entry| -> Result<PathBuf, Box<dyn std::error::Error>> {
            let path = entry.path()?.strip_prefix(prefix)?.to_owned();
            entry.unpack(&path)?;
            Ok(path)
        })
        .filter_map(|e| e.ok())
        .for_each(|x| println!("> {}", x.display()));

    Ok(())
}

並行処理

スレッド

短命なスレッドを生成する

std-badge cat-concurrency-badge

std::thread::scope は新しいスコープ付きスレッドを生成し、そのスレッドはクロージャから戻る前に必ず終了することが保証されています。つまり、Arc やその他の所有権に関するテクニックを使わなくても、呼び出し元の関数のデータを参照できます。

この例では、配列を半分に分割し、それぞれ別のスレッドで処理を実行します。

fn main() {
    let arr = &[1, 25, -4, 10];
    let max = find_max(arr);
    assert_eq!(max, Some(25));
}

fn find_max(arr: &[i32]) -> Option<i32> {
    const THRESHOLD: usize = 2;

    if arr.len() <= THRESHOLD {
        return arr.iter().cloned().max();
    }

    let mid = arr.len() / 2;
    let (left, right) = arr.split_at(mid);

    std::thread::scope(|s| {
        let thread_l = s.spawn(|| find_max(left));
        let thread_r = s.spawn(|| find_max(right));

        let max_l = thread_l.join().unwrap()?;
        let max_r = thread_r.join().unwrap()?;

        Some(max_l.max(max_r))
    })
}

並列パイプラインを作成する

crossbeam-badge cat-concurrency-badge

この例では、crossbeam および crossbeam-channel クレートを使って、 ZeroMQ の guide で説明されているものに似た並列パイプラインを作成します。 データソースとデータシンクがあり、データはソースからシンクへ向かう途中で、 2 つのワーカースレッドによって並列に処理されます。

ここでは、crossbeam_channel::bounded を使って、容量 1 の bounded チャネルを 使用します。プロデューサーは独自のスレッド上になければなりません。これは、 プロデューサーがワーカーよりも速くメッセージを生成するためです (ワーカーは 0.5 秒スリープするため)。つまり、ワーカーのどちらか一方が チャネル内のデータを処理するまで、プロデューサーは crossbeam_channel::Sender::send の呼び出しで 0.5 秒間ブロックされます。 また、チャネル内のデータは先に receive を呼び出したワーカーによって消費されるため、 各メッセージは両方のワーカーではなく、1 つのワーカーにだけ配送されることにも注意してください。

イテレーターメソッド crossbeam_channel::Receiver::iter を介して チャネルから読み取ると、新しいメッセージを待つ間、またはチャネルが閉じられるまで、 処理はブロックされます。チャネルは crossbeam::scope 内で作成されているため、 ワーカーの for ループでプログラム全体がブロックされるのを防ぐには、 drop を使って手動で閉じる必要があります。drop の呼び出しは、 これ以上メッセージが送信されないことを知らせるシグナルと考えることができます。

use std::thread;
use std::time::Duration;
use crossbeam::channel::bounded;

fn main() {
    let (snd1, rcv1) = bounded(1);
    let (snd2, rcv2) = bounded(1);
    let n_msgs = 4;
    let n_workers = 2;

    crossbeam::scope(|s| {
        // プロデューサースレッド
        s.spawn(|_| {
            for i in 0..n_msgs {
                snd1.send(i).unwrap();
                println!("ソースが {} を送信", i);
            }
            // チャネルを閉じる - これはワーカー内の
            // for ループを終了するために必要
            drop(snd1);
        });

        // 2 つのスレッドによる並列処理
        for _ in 0..n_workers {
            // シンクに送信し、ソースから受信
            let (sendr, recvr) = (snd2.clone(), rcv1.clone());
            // 別々のスレッドでワーカーを生成
            s.spawn(move |_| {
                thread::sleep(Duration::from_millis(500));
                // チャネルが閉じられるまで受信
                for msg in recvr.iter() {
                    println!("ワーカー {:?} が {} を受信しました。",
                             thread::current().id(), msg);
                    sendr.send(msg * 2).unwrap();
                }
            });
        }
        // チャネルを閉じる。そうしないとシンクが
        // for ループを終了できない
        drop(snd2);

        // シンク
        for msg in rcv2.iter() {
            println!("シンクが {} を受信", msg);
        }
    }).unwrap();
}

2 つのスレッド間でデータを受け渡す

crossbeam-badge cat-concurrency-badge

この例では、単一プロデューサー・単一コンシューマー(SPSC)の設定で crossbeam-channel を使用する方法を示します。ex-crossbeam-spawn の例をベースに、crossbeam::scopeScope::spawn を使ってプロデューサースレッドを管理します。データは crossbeam_channel::unbounded チャネルを使って 2 つのスレッド間でやり取りされます。これは、格納可能なメッセージ数に制限がないことを意味します。プロデューサースレッドは、各メッセージの間で 0.5 秒スリープします。

use std::{thread, time};
use crossbeam::channel::unbounded;

fn main() {
    let (snd, rcv) = unbounded();
    let n_msgs = 5;
    crossbeam::scope(|s| {
        s.spawn(|_| {
            for i in 0..n_msgs {
                snd.send(i).unwrap();
                thread::sleep(time::Duration::from_millis(100));
            }
        });
    }).unwrap();
    for _ in 0..n_msgs {
        let msg = rcv.recv().unwrap();
        println!("Received {}", msg);
    }
}

グローバルな可変状態を維持する

lazy_static-badge cat-rust-patterns-badge

lazy_static を使ってグローバルな状態を宣言します。lazy_static は、グローバルに利用可能な static ref を作成します。これを変更可能にするには Mutex が必要です(RwLock も参照してください)。Mutex によるラップにより、複数のスレッドから同時に状態へアクセスできなくなり、競合状態を防ぎます。Mutex に格納された値を読み取ったり変更したりするには、MutexGuard を取得する必要があります。

use anyhow::{Result, anyhow};
use lazy_static::lazy_static;
use std::sync::Mutex;

lazy_static! {
    static ref FRUIT: Mutex<Vec<String>> = Mutex::new(Vec::new());
}

fn insert(fruit: &str) -> Result<()> {
    let mut db = FRUIT.lock().map_err(|_| anyhow!("Failed to acquire MutexGuard"))?;
    db.push(fruit.to_string());
    Ok(())
}

fn main() -> Result<()> {
    insert("apple")?;
    insert("orange")?;
    insert("peach")?;
    {
        let db = FRUIT.lock().map_err(|_| anyhow!("Failed to acquire MutexGuard"))?;

        db.iter().enumerate().for_each(|(i, item)| println!("{}: {}", i, item));
    }
    insert("grape")?;
    Ok(())
}

iso ファイルの SHA256 を並行して計算する

threadpool-badge num_cpus-badge walkdir-badge ring-badge cat-concurrency-badgecat-filesystem-badge

この例では、現在のディレクトリにある iso 拡張子を持つすべてのファイルについて SHA256 を計算します。スレッドプールは、num_cpus::get で取得したシステム内のコア数と同じ数のスレッドを生成します。Walkdir::new は現在のディレクトリを走査し、execute を呼び出して、読み込みと SHA256 ハッシュの計算を実行します。

use walkdir::WalkDir;
use std::fs::File;
use std::io::{BufReader, Read, Error};
use std::path::Path;
use threadpool::ThreadPool;
use std::sync::mpsc::channel;
use ring::digest::{Context, Digest, SHA256};

// iso 拡張子を検証する
fn is_iso(entry: &Path) -> bool {
    match entry.extension() {
        Some(e) if e.to_string_lossy().to_lowercase() == "iso" => true,
        _ => false,
    }
}

fn compute_digest<P: AsRef<Path>>(filepath: P) -> Result<(Digest, P), Error> {
    let mut buf_reader = BufReader::new(File::open(&filepath)?);
    let mut context = Context::new(&SHA256);
    let mut buffer = [0; 1024];

    loop {
        let count = buf_reader.read(&mut buffer)?;
        if count == 0 {
            break;
        }
        context.update(&buffer[..count]);
    }

    Ok((context.finish(), filepath))
}

fn main() -> Result<(), Error> {
    let pool = ThreadPool::new(num_cpus::get());

    let (tx, rx) = channel();

    for entry in WalkDir::new("/home/user/Downloads")
        .follow_links(true)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| !e.path().is_dir() && is_iso(e.path())) {
            let path = entry.path().to_owned();
            let tx = tx.clone();
            pool.execute(move || {
                let digest = compute_digest(path);
                tx.send(digest).expect("Could not send data!");
            });
        }

    drop(tx);
    for t in rx.iter() {
        let (sha, path) = t?;
        println!("{:?} {:?}", sha, path);
    }
    Ok(())
}

作業をスレッドプールにディスパッチしてフラクタルを描画する

threadpool-badge num-badge num_cpus-badge image-badge cat-concurrency-badgecat-science-badgecat-rendering-badge

この例では、分散計算のためにスレッドプールを使用して、Julia set からフラクタルを描画し、画像を生成します。

指定した幅と高さの出力画像用のメモリを ImageBuffer::new で確保します。 Rgb::from_channels は RGB ピクセル値を計算します。 num_cpus::get で取得したコア数と同じスレッド数で ThreadPool を作成します。 ThreadPool::execute は各ピクセルを個別のジョブとして受け取ります。

mpsc::channel はジョブを受信し、Receiver::recv はそれらを取り出します。 ImageBuffer::put_pixel はそのデータを使ってピクセルの色を設定します。 ImageBuffer::save は画像を output.png に書き込みます。

use anyhow::Result;
use std::sync::mpsc::channel;
use threadpool::ThreadPool;
use num::complex::Complex;
use image::{ImageBuffer, Pixel, Rgb};

// 輝度値を RGB に変換する関数
// http://www.efg2.com/Lab/ScienceAndEngineering/Spectra.htm に基づく
fn wavelength_to_rgb(wavelength: u32) -> Rgb<u8> {
    let wave = wavelength as f32;

    let (r, g, b) = match wavelength {
        380..=439 => ((440. - wave) / (440. - 380.), 0.0, 1.0),
        440..=489 => (0.0, (wave - 440.) / (490. - 440.), 1.0),
        490..=509 => (0.0, 1.0, (510. - wave) / (510. - 490.)),
        510..=579 => ((wave - 510.) / (580. - 510.), 1.0, 0.0),
        580..=644 => (1.0, (645. - wave) / (645. - 580.), 0.0),
        645..=780 => (1.0, 0.0, 0.0),
        _ => (0.0, 0.0, 0.0),
    };

    let factor = match wavelength {
        380..=419 => 0.3 + 0.7 * (wave - 380.) / (420. - 380.),
        701..=780 => 0.3 + 0.7 * (780. - wave) / (780. - 700.),
        _ => 1.0,
    };

    let (r, g, b) = (normalize(r, factor), normalize(g, factor), normalize(b, factor));
    Rgb([r, g, b])
}

// Julia set の距離推定を輝度値にマッピングする
fn julia(c: Complex<f32>, x: u32, y: u32, width: u32, height: u32, max_iter: u32) -> u32 {
    let width = width as f32;
    let height = height as f32;

    let mut z = Complex {
        // 点を画像座標にスケーリングして平行移動する
        re: 3.0 * (x as f32 - 0.5 * width) / width,
        im: 2.0 * (y as f32 - 0.5 * height) / height,
    };

    let mut i = 0;
    for t in 0..max_iter {
        if z.norm() >= 2.0 {
            break;
        }
        z = z * z + c;
        i = t;
    }
    i
}

// 色の輝度値を RGB の範囲内に正規化する
fn normalize(color: f32, factor: f32) -> u8 {
    ((color * factor).powf(0.8) * 255.) as u8
}

fn main() -> Result<()> {
    let (width, height) = (1920, 1080);
    let mut img = ImageBuffer::new(width, height);
    let iterations = 300;

    let c = Complex::new(-0.8, 0.156);

    let pool = ThreadPool::new(num_cpus::get());
    let (tx, rx) = channel();

    for y in 0..height {
        let tx = tx.clone();
        pool.execute(move || for x in 0..width {
                         let i = julia(c, x, y, width, height, iterations);
                         let pixel = wavelength_to_rgb(380 + i * 400 / iterations);
                         tx.send((x, y, pixel)).expect("データを送信できませんでした!");
                     });
    }

    for _ in 0..(width * height) {
        let (x, y, pixel) = rx.recv()?;
        img.put_pixel(x, y, pixel);
    }
    let _ = img.save("output.png")?;
    Ok(())
}

並列タスク

配列の要素を並列に変更する

rayon-badge cat-concurrency-badge

この例では、Rust向けのデータ並列処理ライブラリである rayon クレートを使用します。 rayon は、並列イテレーション可能な任意のデータ型に対して par_iter_mut メソッドを提供します。 これは、並列に実行される可能性があるイテレータのようなチェーンです。

use rayon::prelude::*;

fn main() {
    let mut arr = [0, 7, 9, 11];
    arr.par_iter_mut().for_each(|p| *p -= 1);
    println!("{:?}", arr);
}

コレクションのいずれかまたはすべての要素が、指定された述語を満たすかどうかを並列にテストする

rayon-badge cat-concurrency-badge

この例では、std::anystd::all に対応する並列版である rayon::any メソッドと rayon::all メソッドの使い方を示します。 rayon::any は、イテレータのいずれかの要素が述語を満たすかどうかを並列に確認し、1 つ見つかるとすぐに返ります。 rayon::all は、イテレータのすべての要素が述語を満たすかどうかを並列に確認し、条件に一致しない要素が見つかるとすぐに返ります。

use rayon::prelude::*;

fn main() {
    let mut vec = vec![2, 4, 6, 8];

    assert!(!vec.par_iter().any(|n| (*n % 2) != 0));
    assert!(vec.par_iter().all(|n| (*n % 2) == 0));
    assert!(!vec.par_iter().any(|n| *n > 8 ));
    assert!(vec.par_iter().all(|n| *n <= 8 ));

    vec.push(9);

    assert!(vec.par_iter().any(|n| (*n % 2) != 0));
    assert!(!vec.par_iter().all(|n| (*n % 2) == 0));
    assert!(vec.par_iter().any(|n| *n > 8 ));
    assert!(!vec.par_iter().all(|n| *n <= 8 )); 
}

与えられた述語を使って並列に項目を検索する

rayon-badge cat-concurrency-badge

この例では、rayon::find_anypar_iter を使って、与えられたクロージャ内の述語を満たす要素をベクターから並列に検索します。

rayon::find_any のクロージャ引数で定義された述語を満たす要素が複数ある場合、rayon は最初の要素を返すとは限らず、見つかったもののうち最初のものを返します。

また、クロージャへの引数は参照への参照(&&x)であることにも注意してください。追加の詳細については std::find の説明を参照してください。

use rayon::prelude::*;

fn main() {
    let v = vec![6, 2, 1, 9, 3, 8, 11];

    let f1 = v.par_iter().find_any(|&&x| x == 9);
    let f2 = v.par_iter().find_any(|&&x| x % 2 == 0 && x > 6);
    let f3 = v.par_iter().find_any(|&&x| x > 8);

    assert_eq!(f1, Some(&9));
    assert_eq!(f2, Some(&8));
    assert!(f3 > Some(&8));
}

ベクターを並列にソートする

rayon-badge rand-badge cat-concurrency-badge

この例では、String のベクターを並列にソートします。

空の String のベクターを確保します。par_iter_mut().for_each はランダムな 値を並列に設定します。列挙可能なデータ型をソートするには multiple options がありますが、par_sort_unstable は通常、stable sorting アルゴリズム より高速です。

use rand::RngExt;
use rayon::prelude::*;

fn main() {
    let mut vec = vec![0; 1_000_000];
    rand::rng().fill(&mut vec[..]);

    vec.par_sort_unstable();

    let first = vec.first().unwrap();
    let last = vec.last().unwrap();
    assert!(first <= last);
}

並列での Map-reduce

rayon-badge cat-concurrency-badge

この例では、rayon::filterrayon::maprayon::reduce を使用して、 年齢が 30 を超える Person オブジェクトの平均年齢を計算します。

rayon::filter は、指定された 述語を満たす要素をコレクションから返します。rayon::map は各要素に対して操作を実行し、新しい イテレーションを作成します。rayon::reduce は、前回の リダクション結果と現在の要素を受け取って操作を実行します。また、rayon::sum の使用例も示しており、 これはこの例では reduce 操作と同じ結果になります。

use rayon::prelude::*;

struct Person {
    age: u32,
}

fn main() {
    let v: Vec<Person> = vec![
        Person { age: 23 },
        Person { age: 19 },
        Person { age: 42 },
        Person { age: 17 },
        Person { age: 17 },
        Person { age: 31 },
        Person { age: 30 },
    ];

    let num_over_30 = v.par_iter().filter(|&x| x.age > 30).count() as f32;
    let sum_over_30 = v.par_iter()
        .map(|x| x.age)
        .filter(|&x| x > 30)
        .reduce(|| 0, |x, y| x + y);

    let alt_sum_30: u32 = v.par_iter()
        .map(|x| x.age)
        .filter(|&x| x > 30)
        .sum();

    let avg_over_30 = sum_over_30 as f32 / num_over_30;
    let alt_avg_over_30 = alt_sum_30 as f32/ num_over_30;

    assert!((avg_over_30 - alt_avg_over_30).abs() < std::f32::EPSILON);
    println!("The average age of people older than 30 is {}", avg_over_30);
}

並列に jpg サムネイルを生成する

rayon-badge glob-badge image-badge cat-concurrency-badge cat-filesystem-badge

この例では、現在のディレクトリ内にあるすべての .jpg ファイルのサムネイルを生成し、 それらを thumbnails という新しいフォルダーに保存します。

glob::glob_with は現在のディレクトリ内の jpeg ファイルを見つけます。rayonDynamicImage::resize を呼び出す par_iter を使用して、画像のサイズを並列に変更します。

use anyhow::Result;
use std::path::Path;
use std::fs::create_dir_all;

use glob::{glob_with, MatchOptions};
use image::imageops::FilterType;
use rayon::prelude::*;

fn main() -> Result<()> {
    let options: MatchOptions = Default::default();
    let files: Vec<_> = glob_with("*.jpg", options)?
        .filter_map(|x| x.ok())
        .collect();

    if files.len() == 0 {
        anyhow::bail!("No .jpg files found in current directory");
    }

    let thumb_dir = "thumbnails";
    create_dir_all(thumb_dir)?;

    println!("Saving {} thumbnails into '{}'...", files.len(), thumb_dir);

    let image_failures: Vec<_> = files
        .par_iter()
        .map(|path| {
            make_thumbnail(path, thumb_dir, 300)
                .map_err(|e| anyhow::anyhow!("Failed to process {}: {}", path.display(), e))
        })
        .filter_map(|x| x.err())
        .collect();

    image_failures.iter().for_each(|x| println!("{}", x));

    println!("{} thumbnails saved successfully", files.len() - image_failures.len());
    Ok(())
}

fn make_thumbnail<PA, PB>(original: PA, thumb_dir: PB, longest_edge: u32) -> Result<()>
where
    PA: AsRef<Path>,
    PB: AsRef<Path>,
{
    let img = image::open(original.as_ref())?;
    let file_path = thumb_dir.as_ref().join(original);

    Ok(img.resize(longest_edge, longest_edge, FilterType::Nearest)
        .save(file_path)?)
}

アクターパターン

Tokio を用いたアクターパターン(ハンドル/アクター/メッセージ)

tokio-badge cat-concurrency-badge cat-rust-patterns-badge

Arc<Mutex<T>> を使わずに共有可変状態を管理する一般的なパターンの 1 つが、Alice Ryhl によって広く知られるようになったアクターパターンです。アクターは自身のデータを所有する struct であり、単一の spawn されたタスク内で実行されます。クローン可能な ハンドルmpsc::Sender を保持し、公開 API となります。メッセージ は、アクターが処理できるすべてのコマンドを記述する enum です。

データにアクセスするのは常に 1 つのタスクだけであるため、ロックは不要です。リクエスト–レスポンスの組は、メッセージバリアントに埋め込まれた oneshot チャネルを使用します。

use thiserror::Error;
use tokio::sync::{mpsc, oneshot};

#[derive(Debug, Error)]
enum ActorError {
    #[error("failed to send message to actor")]
    Send(#[from] mpsc::error::SendError<Message>),
    #[error("actor dropped response channel")]
    Recv(#[from] oneshot::error::RecvError),
    #[error("task failed")]
    Join(#[from] tokio::task::JoinError),
}

// The Message enum represents commands sent to the actor.
enum Message {
    UpdateLocation {
        driver_id: u32,
        lat: f64,
        lng: f64,
    },
    GetDriverStatus {
        driver_id: u32,
        respond_to: oneshot::Sender<Option<DriverStatus>>,
    },
}

#[derive(Debug, Clone)]
struct DriverStatus {
    driver_id: u32,
    lat: f64,
    lng: f64,
    update_count: u64,
}

// The Actor: a struct that owns data and lives inside a spawned task.
// No Arc<Mutex> needed—only one task accesses the data.
struct DriverActor {
    receiver: mpsc::Receiver<Message>,
    drivers: std::collections::HashMap<u32, DriverStatus>,
}

impl DriverActor {
    fn new(receiver: mpsc::Receiver<Message>) -> Self {
        Self {
            receiver,
            drivers: std::collections::HashMap::new(),
        }
    }

    async fn run(&mut self) {
        while let Some(msg) = self.receiver.recv().await {
            self.handle_message(msg);
        }
    }

    fn handle_message(&mut self, msg: Message) {
        match msg {
            Message::UpdateLocation {
                driver_id,
                lat,
                lng,
            } => {
                let status = self
                    .drivers
                    .entry(driver_id)
                    .or_insert(DriverStatus {
                        driver_id,
                        lat: 0.0,
                        lng: 0.0,
                        update_count: 0,
                    });
                status.lat = lat;
                status.lng = lng;
                status.update_count += 1;
            }
            Message::GetDriverStatus {
                driver_id,
                respond_to,
            } => {
                let status = self.drivers.get(&driver_id).cloned();
                // Ignore send errors if the caller dropped the receiver.
                let _ = respond_to.send(status);
            }
        }
    }
}

// The Handle: a clonable struct that holds an mpsc::Sender.
// This is what you pass around the application.
#[derive(Clone)]
struct DriverHandle {
    sender: mpsc::Sender<Message>,
}

impl DriverHandle {
    fn new() -> Self {
        let (sender, receiver) = mpsc::channel(32);
        let mut actor = DriverActor::new(receiver);
        tokio::spawn(async move { actor.run().await });
        Self { sender }
    }

    async fn update_location(
        &self,
        driver_id: u32,
        lat: f64,
        lng: f64,
    ) -> Result<(), ActorError> {
        self.sender
            .send(Message::UpdateLocation {
                driver_id,
                lat,
                lng,
            })
            .await?;
        Ok(())
    }

    async fn get_driver_status(
        &self,
        driver_id: u32,
    ) -> Result<Option<DriverStatus>, ActorError> {
        let (tx, rx) = oneshot::channel();
        self.sender
            .send(Message::GetDriverStatus {
                driver_id,
                respond_to: tx,
            })
            .await?;
        Ok(rx.await?)
    }
}

#[tokio::main]
async fn main() -> Result<(), ActorError> {
    let handle = DriverHandle::new();

    // Multiple clones can be sent to different tasks.
    let h1 = handle.clone();
    let h2 = handle.clone();

    let task1 = tokio::spawn(async move {
        h1.update_location(1, 40.7128, -74.0060).await?;
        h1.update_location(1, 40.7130, -74.0062).await?;
        Ok::<(), ActorError>(())
    });

    let task2 = tokio::spawn(async move {
        h2.update_location(2, 34.0522, -118.2437).await?;
        Ok::<(), ActorError>(())
    });

    task1.await??;
    task2.await??;

    if let Some(s) = handle.get_driver_status(1).await? {
        println!("Driver {}: ({}, {}), updates: {}", s.driver_id, s.lat, s.lng, s.update_count);
    }

    if let Some(s) = handle.get_driver_status(2).await? {
        println!("Driver {}: ({}, {}), updates: {}", s.driver_id, s.lat, s.lng, s.update_count);
    }

    let missing = handle.get_driver_status(99).await?;
    println!("Driver 99: {:?}", missing);

    Ok(())
}

カスタム Future

カスタム Future を実装する (Pin, Waker, Poll)

std-badge cat-concurrency-badge cat-rust-patterns-badge

低レベルな制御が必要な場合、たとえばカスタムのハードウェアドライバー、特殊なタイマー、 あるいはゼロアロケーションのプロトコルパーサーでは、async/await に頼る代わりに Future を手で実装できます。

すべてのカスタム future は、3 つの概念を扱う必要があります:

概念重要である理由
Pin<&mut Self>最初の poll の後に future がメモリ上で移動しないことを保証します。これは自己参照する future(たとえば .await ポイントをまたいで borrow を保持するもの)にとって重要です。構造体に通常のフィールドしか含まれていない場合(自己参照がない場合)、コンパイラは Unpin を自動実装するため、pinning は実質的に no-op です。
Poll::Pending / Poll::ReadyPending は executor に「まだ完了していない」ことを伝え、Ready(value) は future を完了させます。
cx.waker()executor が渡してくるハンドルです。Pending を返した後は、どこかの時点で必ず wake() を呼び出さなければなりません。そうしないと executor はその future を二度と poll せず、処理は停止したままになります。

以下の例では、期限を過ぎた後に完了する単純な Delay future を構築します。
これは自己参照フィールドを持たないため、自動的に Unpin になります— pinning にコストはかかりません。実運用のタイマーであれば reactor に登録するでしょう。 ここでは executor が再度 poll するように、ただちに wake_by_ref() を呼び出します。

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};

/// A hand-rolled future that resolves after a deadline.
///
/// This is intentionally simple—no timer wheel, no reactor—just a
/// busy-polling future that shows the three things every custom
/// `Future` must handle:
///
/// 1. **`Pin<&mut Self>`** — guarantees our struct won't move in
///    memory.  Because `Delay` contains only `Instant` and
///    `Duration` (both `Unpin`), the compiler auto-implements
///    `Unpin` for us and pinning is a no-op here.  If the struct
///    held a self-referential borrow (like a hand-written async
///    state machine), pinning would *prevent* the struct from
///    being moved after the first poll, which would invalidate
///    the borrow.
///
/// 2. **`Poll::Pending` vs `Poll::Ready`** — returning `Pending`
///    tells the executor "I'm not done yet; wake me later."
///
/// 3. **`cx.waker()`** — the mechanism to *schedule* a re-poll.
///    Without calling `wake()`, the executor would never poll us
///    again and the future would hang.
struct Delay {
    /// When we should resolve.
    deadline: Instant,
}

impl Delay {
    fn new(dur: Duration) -> Self {
        Self {
            deadline: Instant::now() + dur,
        }
    }
}

impl Future for Delay {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.deadline {
            // Deadline reached — resolve the future.
            Poll::Ready(())
        } else {
            // Not ready yet.
            //
            // We MUST arrange for `wake()` to be called later or the
            // executor will never poll us again.  A production timer
            // would register with a reactor; here we just ask to be
            // re-polled immediately. The executor may yield to other
            // tasks before coming back to us.
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

#[tokio::main]
async fn main() {
    let start = Instant::now();
    println!("waiting 10 ms …");

    // Use the custom future just like any other async expression.
    Delay::new(Duration::from_millis(10)).await;

    println!("done in {:?}", start.elapsed());
    assert!(start.elapsed() >= Duration::from_millis(10));
}

暗号

ハッシュ化

ファイルの SHA-256 ダイジェストを計算する

ring-badge data-encoding-badge cat-cryptography-badge

いくつかのデータをファイルに書き込み、その後 digest::Context を使用して ファイルの内容の SHA-256 digest::Digest を計算します。

use anyhow::Result;
use ring::digest::{Context, Digest, SHA256};
use data_encoding::HEXUPPER;
use std::fs::File;
use std::io::{BufReader, Read, Write};

fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest> {
    let mut context = Context::new(&SHA256);
    let mut buffer = [0; 1024];

    loop {
        let count = reader.read(&mut buffer)?;
        if count == 0 {
            break;
        }
        context.update(&buffer[..count]);
    }

    Ok(context.finish())
}

fn main() -> Result<()> {
    let path = "file.txt";

    let mut output = File::create(path)?;
    write!(output, "このテキストのダイジェストを生成します")?;

    let input = File::open(path)?;
    let reader = BufReader::new(input);
    let digest = sha256_digest(reader)?;

    println!("SHA-256 ダイジェストは {} です", HEXUPPER.encode(digest.as_ref()));

    Ok(())
}

HMAC ダイジェストでメッセージに署名して検証する

ring-badge cat-cryptography-badge

hmac::sign メソッドは、指定されたキーを使用してメッセージの HMAC ダイジェスト(タグとも呼ばれます)を計算するために使用されます。 生成された hmac::Tag 構造体には HMAC の生のバイト列が含まれており、 後からhmac::verifyで検証することで、メッセージが改ざんされておらず、信頼できる送信元から送られてきたことを確認できます。

use ring::{hmac, rand};
use ring::rand::SecureRandom;
use ring::error::Unspecified;

fn main() -> Result<(), Unspecified> {
    let mut key_value = [0u8; 48];
    let rng = rand::SystemRandom::new();
    rng.fill(&mut key_value)?;
    let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value);

    let message = "Legitimate and important message.";
    let signature: hmac::Tag = hmac::sign(&key, message.as_bytes());
    hmac::verify(&key, message.as_bytes(), signature.as_ref())?;

    Ok(())
}

暗号化

PBKDF2でパスワードをソルト化してハッシュ化する

ring-badge data-encoding-badge cat-cryptography-badge

ring::pbkdf2 を使用して、PBKDF2 鍵導出関数 pbkdf2::derive によりソルト付きパスワードをハッシュ化します。[pbkdf2::verify] を使ってハッシュが正しいことを 検証します。ソルトは SecureRandom::fill を使用して生成され、 これによりソルトのバイト配列が安全に生成された乱数で 埋められます。

use data_encoding::HEXUPPER;
use ring::error::Unspecified;
use ring::rand::SecureRandom;
use ring::{digest, pbkdf2, rand};
use std::num::NonZeroU32;

fn main() -> Result<(), Unspecified> {
    const CREDENTIAL_LEN: usize = digest::SHA512_OUTPUT_LEN;
    let n_iter = NonZeroU32::new(100_000).unwrap();
    let rng = rand::SystemRandom::new();

    let mut salt = [0u8; CREDENTIAL_LEN];
    rng.fill(&mut salt)?;

    let password = "Guess Me If You Can!";
    let mut pbkdf2_hash = [0u8; CREDENTIAL_LEN];
    pbkdf2::derive(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        password.as_bytes(),
        &mut pbkdf2_hash,
    );
    println!("Salt: {}", HEXUPPER.encode(&salt));
    println!("PBKDF2 hash: {}", HEXUPPER.encode(&pbkdf2_hash));

    let should_succeed = pbkdf2::verify(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        password.as_bytes(),
        &pbkdf2_hash,
    );
    let wrong_password = "Definitely not the correct password";
    let should_fail = pbkdf2::verify(
        pbkdf2::PBKDF2_HMAC_SHA512,
        n_iter,
        &salt,
        wrong_password.as_bytes(),
        &pbkdf2_hash,
    );

    assert!(should_succeed.is_ok());
    assert!(!should_fail.is_ok());

    Ok(())
}

データ構造

ビットフィールド

ビットフィールドとして表現される型を定義して操作する

bitflags-badge cat-no-std-badge

基本的な clear 操作と [Display] トレイトを備えた、型安全なビットフィールド型 MyFlags を作成します。 続いて、基本的なビット単位演算とフォーマットを示します。

use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct MyFlags(u32);

impl MyFlags {
    const FLAG_A: MyFlags = MyFlags(0b00000001);
    const FLAG_B: MyFlags = MyFlags(0b00000010);
    const FLAG_C: MyFlags = MyFlags(0b00000100);
    const FLAG_ABC: MyFlags = MyFlags(Self::FLAG_A.0 | Self::FLAG_B.0 | Self::FLAG_C.0);

    fn empty() -> Self {
        MyFlags(0)
    }

    fn bits(&self) -> u32 {
        self.0
    }

    pub fn clear(&mut self) -> &mut MyFlags {
        *self = MyFlags::empty();
        self
    }
}

impl std::ops::BitOr for MyFlags {
    type Output = Self;
    fn bitor(self, rhs: Self) -> Self {
        MyFlags(self.0 | rhs.0)
    }
}

impl std::ops::BitAnd for MyFlags {
    type Output = Self;
    fn bitand(self, rhs: Self) -> Self {
        MyFlags(self.0 & rhs.0)
    }
}

impl std::ops::Sub for MyFlags {
    type Output = Self;
    fn sub(self, rhs: Self) -> Self {
        MyFlags(self.0 & !rhs.0)
    }
}

impl std::ops::Not for MyFlags {
    type Output = Self;
    fn not(self) -> Self {
        MyFlags(!self.0 & 0b00000111) // 定義されているフラグのみを考慮する
    }
}

impl fmt::Display for MyFlags {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:032b}", self.bits())
    }
}

fn main() {
    let e1 = MyFlags::FLAG_A | MyFlags::FLAG_C;
    let e2 = MyFlags::FLAG_B | MyFlags::FLAG_C;
    assert_eq!((e1 | e2), MyFlags::FLAG_ABC);
    assert_eq!((e1 & e2), MyFlags::FLAG_C);
    assert_eq!((e1 - e2), MyFlags::FLAG_A);
    assert_eq!(!e2, MyFlags::FLAG_A);

    let mut flags = MyFlags::FLAG_ABC;
    assert_eq!(format!("{}", flags), "00000000000000000000000000000111");
    assert_eq!(format!("{}", flags.clear()), "00000000000000000000000000000000");
    assert_eq!(format!("{:?}", MyFlags::FLAG_B), "MyFlags(2)");
    assert_eq!(format!("{:?}", MyFlags::FLAG_A | MyFlags::FLAG_B), "MyFlags(3)");
}

データベース

SQLite

SQLite データベースを作成する

rusqlite-badge cat-database-badge

rusqlite クレートを使用して SQLite データベースを開きます。Windows でのコンパイルについては、 クレート を参照してください。

Connection::open は、データベースがまだ存在しない場合、それを作成します。

use rusqlite::{Connection, Result};

fn main() -> Result<()> {
    let conn = Connection::open("cats.db")?;

    conn.execute(
        "create table if not exists cat_colors (
             id integer primary key,
             name text not null unique
         )",
        (),
    )?;
    conn.execute(
        "create table if not exists cats (
             id integer primary key,
             name text not null,
             color_id integer not null references cat_colors(id)
         )",
        (),
    )?;

    Ok(())
}

データの挿入と選択

rusqlite-badge cat-database-badge

Connection::open は、前のレシピで作成したデータベース cats を開きます。 このレシピでは、Connectionexecute メソッドを使用して、cat_colors テーブルと cats テーブルにデータを挿入します。まず、cat_colors テーブルにデータを挿入します。色のレコードが挿入された後、Connectionlast_insert_rowid メソッドを使用して、最後に挿入された色の id を取得します。この id は、cats テーブルにデータを挿入する際に使用されます。次に、prepare メソッドを使用して select クエリを準備します。これにより statement 構造体が得られます。続いて、statementquery_map メソッドを使用してクエリを実行します。

use rusqlite::{params, Connection, Result};
use std::collections::HashMap;

#[derive(Debug)]
struct Cat {
    name: String,
    color: String,
}

fn main() -> Result<()> {
    let conn = Connection::open("cats.db")?;

    let mut cat_colors = HashMap::new();
    cat_colors.insert(String::from("Blue"), vec!["Tigger", "Sammy"]);
    cat_colors.insert(String::from("Black"), vec!["Oreo", "Biscuit"]);

    for (color, catnames) in &cat_colors {
        conn.execute(
            "INSERT INTO cat_colors (name) VALUES (?1)",
            [color],
        )?;
        let last_id = conn.last_insert_rowid();

        for cat in catnames {
            conn.execute(
                "INSERT INTO cats (name, color_id) values (?1, ?2)",
                params![cat, last_id],
            )?;
        }
    }
    let mut stmt = conn.prepare(
        "SELECT c.name, cc.name FROM cats c
         INNER JOIN cat_colors cc
         ON cc.id = c.color_id;",
    )?;

    let cats = stmt.query_map([], |row| {
        Ok(Cat {
            name: row.get(0)?,
            color: row.get(1)?,
        })
    })?;

    for cat in cats {
        if let Ok(found_cat) = cat {
            println!(
                "Found cat {:?} {} is {}", 
                found_cat,
                found_cat.name,
                found_cat.color,
                );
        }
    }

    Ok(())
}

トランザクションの使用

rusqlite-badge cat-database-badge

Connection::open は、冒頭のレシピにある cats.db データベースを開きます。

トランザクションは Connection::transaction で開始します。トランザクションは Transaction::commit で明示的にコミットしない限りロールバックされます。

次の例では、色名に一意制約があるテーブルに 色を追加します。重複する色を挿入しようとすると、 トランザクションはロールバックされます。

use rusqlite::{Connection, Result};

fn main() -> Result<()> {
    let mut conn = Connection::open("cats.db")?;

    successful_tx(&mut conn)?;

    let res = rolled_back_tx(&mut conn);
    assert!(res.is_err());

    Ok(())
}

fn successful_tx(conn: &mut Connection) -> Result<()> {
    let tx = conn.transaction()?;

    tx.execute("delete from cat_colors", [])?;
    tx.execute("insert into cat_colors (name) values (?1)", ["lavender"])?;
    tx.execute("insert into cat_colors (name) values (?1)", ["blue"])?;

    tx.commit()
}

fn rolled_back_tx(conn: &mut Connection) -> Result<()> {
    let tx = conn.transaction()?;

    tx.execute("delete from cat_colors", [])?;
    tx.execute("insert into cat_colors (name) values (?1)", ["lavender"])?;
    tx.execute("insert into cat_colors (name) values (?1)", ["blue"])?;
    tx.execute("insert into cat_colors (name) values (?1)", ["lavender"])?;

    tx.commit()
}

Postgres を使う

Postgres データベースにテーブルを作成する

postgres-badge cat-database-badge

Postgres データベースにテーブルを作成するには postgres クレートを使用します。

Client::connect は既存のデータベースへの接続に役立ちます。このレシピでは、Client::connect で URL 文字列形式を使用しています。既存の library という名前のデータベースがあり、ユーザー名が postgres、パスワードが postgres であることを前提としています。

use postgres::{Client, NoTls, Error};

fn main() -> Result<(), Error> {
    let mut client = Client::connect("postgresql://postgres:postgres@localhost/library", NoTls)?;
    
    client.batch_execute("
        CREATE TABLE IF NOT EXISTS author (
            id              SERIAL PRIMARY KEY,
            name            VARCHAR NOT NULL,
            country         VARCHAR NOT NULL
            )
    ")?;

    client.batch_execute("
        CREATE TABLE IF NOT EXISTS book  (
            id              SERIAL PRIMARY KEY,
            title           VARCHAR NOT NULL,
            author_id       INTEGER NOT NULL REFERENCES author
            )
    ")?;

    Ok(())

}

データの挿入とクエリ

postgres-badge cat-database-badge

このレシピでは、Clientexecute メソッドを使用して author テーブルにデータを挿入します。次に、Clientquery メソッドを使用して author テーブルのデータを表示します。

use postgres::{Client, NoTls, Error};
use std::collections::HashMap;

struct Author {
    _id: i32,
    name: String,
    country: String
}

fn main() -> Result<(), Error> {
    let mut client = Client::connect("postgresql://postgres:postgres@localhost/library", 
                                    NoTls)?;
    
    let mut authors = HashMap::new();
    authors.insert(String::from("Chinua Achebe"), "Nigeria");
    authors.insert(String::from("Rabindranath Tagore"), "India");
    authors.insert(String::from("Anita Nair"), "India");

    for (key, value) in &authors {
        let author = Author {
            _id: 0,
            name: key.to_string(),
            country: value.to_string()
        };

        client.execute(
                "INSERT INTO author (name, country) VALUES ($1, $2)",
                &[&author.name, &author.country],
        )?;
    }

    for row in client.query("SELECT id, name, country FROM author", &[])? {
        let author = Author {
            _id: row.get(0),
            name: row.get(1),
            country: row.get(2),
        };
        println!("Author {} is from {}", author.name, author.country);
    }

    Ok(())

}

集計データ

postgres-badge cat-database-badge

このレシピは、Museum of Modern Art のデータベースに登録されている最初の 7999 人のアーティストの国籍を降順で一覧表示します。

use postgres::{Client, Error, NoTls};

struct Nation {
    nationality: String,
    count: i64,
}

fn main() -> Result<(), Error> {
    let mut client = Client::connect(
        "postgresql://postgres:postgres@127.0.0.1/moma",
        NoTls,
    )?;

    for row in client.query 
	("SELECT nationality, COUNT(nationality) AS count 
	FROM artists GROUP BY nationality ORDER BY count DESC", &[])? {
        
        let (nationality, count) : (Option<String>, Option<i64>) 
		= (row.get (0), row.get (1));
        
        if nationality.is_some () && count.is_some () {

            let nation = Nation {
                nationality: nationality.unwrap(),
                count: count.unwrap(),
            };
            println!("{} {}", nation.nationality, nation.count);
            
        }
    }

    Ok(())
}

日付と時刻

Duration と計算

2 つのコードセクション間の経過時間を測定する

std-badge cat-time-badge

time::Instant::now からの time::Instant::elapsed を測定します。

time::Instant::elapsed を呼び出すと time::Duration が返され、これを例の最後で出力します。 このメソッドは time::Instant オブジェクトを変更したりリセットしたりしません。

use std::time::{Duration, Instant};
use std::thread;

fn expensive_function() {
    thread::sleep(Duration::from_secs(1));
}

fn main() {
    let start = Instant::now();
    expensive_function();
    let duration = start.elapsed();

    println!("Time elapsed in expensive_function() is: {:?}", duration);
}

日付と時刻のチェック付き計算を行う

chrono-badge cat-date-and-time-badge

DateTime::checked_add_signed を使用して今から 2 週間後の日付と時刻を計算して表示し、DateTime::checked_sub_signed を使用してその前日の日付を計算して表示します。日付と時刻を計算できない場合、これらのメソッドは None を返します。

DateTime::format で使用できるエスケープシーケンスは chrono::format::strftime にあります。

use chrono::{DateTime, Duration, Utc};

fn day_earlier(date_time: DateTime<Utc>) -> Option<DateTime<Utc>> {
    date_time.checked_sub_signed(Duration::days(1))
}

fn main() {
    let now = Utc::now();
    println!("{}", now);

    let almost_three_weeks_from_now = now.checked_add_signed(Duration::weeks(2))
            .and_then(|in_2weeks| in_2weeks.checked_add_signed(Duration::weeks(1)))
            .and_then(day_earlier);

    match almost_three_weeks_from_now {
        Some(x) => println!("{}", x),
        None => eprintln!("Almost three weeks from now overflows!"),
    }

    match now.checked_add_signed(Duration::max_value()) {
        Some(x) => println!("{}", x),
        None => eprintln!("We can't use chrono to tell the time for the Solar System to complete more than one full orbit around the galactic center."),
    }
}

ローカル時刻を別のタイムゾーンに変換する

chrono-badge cat-date-and-time-badge

offset::Local::now を使用してローカル時刻を取得して表示し、その後 DateTime::from_utc 構造体メソッドを使用して UTC 標準時に変換します。次に、offset::FixedOffset 構造体を使用して時刻を変換し、UTC 時刻を UTC+8 と UTC-2 に変換します。

use chrono::{DateTime, FixedOffset, Local, Utc};

fn main() {
    let local_time = Local::now();
    let utc_time = DateTime::<Utc>::from_utc(local_time.naive_utc(), Utc);
    let china_timezone = FixedOffset::east(8 * 3600);
    let rio_timezone = FixedOffset::west(2 * 3600);
    println!("Local time now is {}", local_time);
    println!("UTC time now is {}", utc_time);
    println!(
        "Time in Hong Kong now is {}",
        utc_time.with_timezone(&china_timezone)
    );
    println!("Time in Rio de Janeiro now is {}", utc_time.with_timezone(&rio_timezone));
}

パースと表示

日付と時刻を調べる

chrono-badge cat-date-and-time-badge

現在の UTC DateTime を取得し、Timelike を使って時/分/秒を、Datelike を使って年/月/日/曜日を取得します。

use chrono::{Datelike, Timelike, Utc};

fn main() {
    let now = Utc::now();

    let (is_pm, hour) = now.hour12();
    println!(
        "The current UTC time is {:02}:{:02}:{:02} {}",
        hour,
        now.minute(),
        now.second(),
        if is_pm { "PM" } else { "AM" }
    );
    println!(
        "And there have been {} seconds since midnight",
        now.num_seconds_from_midnight()
    );

    let (is_common_era, year) = now.year_ce();
    println!(
        "The current UTC date is {}-{:02}-{:02} {:?} ({})",
        year,
        now.month(),
        now.day(),
        now.weekday(),
        if is_common_era { "CE" } else { "BCE" }
    );
    println!(
        "And the Common Era began {} days ago",
        now.num_days_from_ce()
    );
}

日付を UNIX タイムスタンプに変換し、その逆変換も行う

chrono-badge cat-date-and-time-badge

NaiveDate::from_ymdNaiveTime::from_hms で指定した日付を NaiveDateTime::timestamp を使って UNIX timestamp に変換します。 続いて、1970 年 1 月 1 日 0:00:00 UTC から 10 億秒後の日付が何であったかを NaiveDateTime::from_timestamp を使って計算します。

use chrono::{NaiveDate, NaiveDateTime};

fn main() {
    let date_time: NaiveDateTime = NaiveDate::from_ymd(2017, 11, 12).and_hms(17, 33, 44);
    println!(
        "Number of seconds between 1970-01-01 00:00:00 and {} is {}.",
        date_time, date_time.timestamp());

    let date_time_after_a_billion_seconds = NaiveDateTime::from_timestamp(1_000_000_000, 0);
    println!(
        "Date after a billion seconds since 1970-01-01 00:00:00 was {}.",
        date_time_after_a_billion_seconds);
}

書式設定された日付と時刻を表示

chrono-badge cat-date-and-time-badge

Utc::now を使用して UTC の現在時刻を取得して表示します。現在時刻を、DateTime::to_rfc2822 を使用したよく知られた形式の RFC 2822DateTime::to_rfc3339 を使用した RFC 3339、および DateTime::format を使用したカスタム形式で書式設定します。

use chrono::{DateTime, Utc};

fn main() {
    let now: DateTime<Utc> = Utc::now();

    println!("UTC now is: {}", now);
    println!("UTC now in RFC 2822 is: {}", now.to_rfc2822());
    println!("UTC now in RFC 3339 is: {}", now.to_rfc3339());
    println!("UTC now in a custom format is: {}", now.format("%a %b %e %T %Y"));
}

文字列を DateTime 構造体にパースする

chrono-badge cat-date-and-time-badge

よく知られた形式である RFC 2822RFC 3339、およびカスタム形式を表す文字列から、それぞれ DateTime::parse_from_rfc2822DateTime::parse_from_rfc3339、および DateTime::parse_from_str を使用して DateTime 構造体をパースします。

DateTime::parse_from_str で利用できるエスケープシーケンスは chrono::format::strftime にあります。なお、DateTime::parse_from_str では、 日付と時刻を一意に特定できる DateTime 構造体を生成できる必要があります。 タイムゾーンなしの日付と時刻をパースするには NaiveDateNaiveTime、および NaiveDateTime を使用してください。

use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime};
use chrono::format::ParseError;


fn main() -> Result<(), ParseError> {
    let rfc2822 = DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200")?;
    println!("{}", rfc2822);

    let rfc3339 = DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")?;
    println!("{}", rfc3339);

    let custom = DateTime::parse_from_str("5.8.1994 8:00 am +0000", "%d.%m.%Y %H:%M %P %z")?;
    println!("{}", custom);

    let time_only = NaiveTime::parse_from_str("23:56:04", "%H:%M:%S")?;
    println!("{}", time_only);

    let date_only = NaiveDate::parse_from_str("2015-09-05", "%Y-%m-%d")?;
    println!("{}", date_only);

    let no_timezone = NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")?;
    println!("{}", no_timezone);

    Ok(())
}

開発ツール

デバッグ

バージョニング

ビルド時

デバッグ

デバッグ

ログメッセージ

コンソールにデバッグメッセージをログ出力する

log-badge env_logger-badge cat-debugging-badge

log クレートはロギングユーティリティを提供します。env_logger クレートは 環境変数を介してロギングを設定します。log::debug! マクロは、他の std::fmt のフォーマット文字列と同様に動作します。

fn execute_query(query: &str) {
    log::debug!("Executing query: {}", query);
}

fn main() {
    env_logger::init();

    execute_query("DROP TABLE students");
}

このコードを実行しても、何も出力されません。デフォルトでは、 ログレベルは error であり、それより低いレベルは破棄されます。

メッセージを出力するには、RUST_LOG 環境変数を設定します:

$ RUST_LOG=debug cargo run

すると Cargo はデバッグ情報を出力し、その後、 出力の最後に次の行が表示されます:

DEBUG:main: Executing query: DROP TABLE students

コンソールにエラーメッセージをログ出力する

log-badge env_logger-badge cat-debugging-badge

適切なエラーハンドリングでは、例外的なものだけを例外として扱います。ここでは、エラーを log の便利なマクロ log::error! で標準エラー出力にログ出力します。

fn execute_query(_query: &str) -> Result<(), &'static str> {
    Err("I'm afraid I can't do that")
}

fn main() {
    env_logger::init();

    let response = execute_query("DROP TABLE students");
    if let Err(err) = response {
        log::error!("Failed to execute query: {}", err);
    }
}

stderr ではなく stdout にログを出力する

log-badge env_logger-badge cat-debugging-badge

Builder::target を使用してログ出力先を Target::Stdout に設定する、カスタムロガー構成を作成します。

use env_logger::{Builder, Target};

fn main() {
    Builder::new()
        .target(Target::Stdout)
        .init();

    log::error!("This error has been printed to Stdout");
}

カスタムロガーでログメッセージを出力する

log-badge cat-debugging-badge

stdout に出力するカスタムロガー ConsoleLogger を実装します。 ロギングマクロを使用するために、ConsoleLoggerlog::Log トレイトを実装し、log::set_logger でそれを登録します。

use log::{Record, Level, Metadata, LevelFilter, SetLoggerError};

static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger;

struct ConsoleLogger;

impl log::Log for ConsoleLogger {
  fn enabled(&self, metadata: &Metadata) -> bool {
     metadata.level() <= Level::Info
    }

    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            println!("Rust says: {} - {}", record.level(), record.args());
        }
    }

    fn flush(&self) {}
}

fn main() -> Result<(), SetLoggerError> {
    log::set_logger(&CONSOLE_LOGGER)?;
    log::set_max_level(LevelFilter::Info);

    log::info!("hello log");
    log::warn!("warning");
    log::error!("oops");
    Ok(())
}

Unix syslog にログを出力する

log-badge syslog-badge cat-debugging-badge

UNIX syslog にメッセージを記録します。syslog::init を使用してロガーバックエンドを初期化 します。syslog::Facility はログエントリを送信するプログラムの分類を記録し、 log::LevelFilter は許可されるログの詳細度を示し、 Option<&str> は省略可能なアプリケーション名を保持します。

#[cfg(target_os = "linux")]
#[cfg(target_os = "linux")]
use syslog::{Facility, Error};

#[cfg(target_os = "linux")]
fn main() -> Result<(), Error> {
    syslog::init(Facility::LOG_USER,
                 log::LevelFilter::Debug,
                 Some("My app name"))?;
    log::debug!("this is a debug {}", "message");
    log::error!("this is an error!");
    Ok(())
}

#[cfg(not(target_os = "linux"))]
fn main() {
    println!("So far, only Linux systems are supported.");
}

ロギングを設定する

モジュールごとにログレベルを有効にする

log-badge env_logger-badge cat-debugging-badge

foo と、そのネストされた foo::bar という 2 つのモジュールを作成し、RUST_LOG 環境変数によってログ出力のディレクティブを個別に制御します。

mod foo {
    mod bar {
        pub fn run() {
            log::warn!("[bar] warn");
            log::info!("[bar] info");
            log::debug!("[bar] debug");
        }
    }

    pub fn run() {
        log::warn!("[foo] warn");
        log::info!("[foo] info");
        log::debug!("[foo] debug");
        bar::run();
    }
}

fn main() {
    env_logger::init();
    log::warn!("[root] warn");
    log::info!("[root] info");
    log::debug!("[root] debug");
    foo::run();
}

RUST_LOG 環境変数は env_logger の出力を制御します。 モジュール宣言には、path::to::module=log_level 形式のカンマ区切りエントリを指定します。test アプリケーションは次のように実行します。

RUST_LOG="warn,test::foo=info,test::foo::bar=debug" ./test

これにより、デフォルトの log::Levelwarn に、モジュール fooinfo に、モジュール foo::bardebug に設定されます。

WARN:test: [root] warn
WARN:test::foo: [foo] warn
INFO:test::foo: [foo] info
WARN:test::foo::bar: [bar] warn
INFO:test::foo::bar: [bar] info
DEBUG:test::foo::bar: [bar] debug

カスタム環境変数を使用してロギングを設定する

log-badge env_logger-badge cat-debugging-badge

Builder はロギングを設定します。

Builder::from_envMY_APP_LOG 環境変数の内容を RUST_LOG 構文の形式で解析します。 その後、Builder::init がロガーを初期化します。

use env_logger::Builder;

fn main() {
    Builder::from_env("MY_APP_LOG").init();

    log::info!("informational message");
    log::warn!("warning message");
    log::error!("this is an error {}", "message");
}

ログメッセージにタイムスタンプを含める

log-badge env_logger-badge chrono-badge cat-debugging-badge

Builder を使ってカスタムロガー設定を作成します。 各ログエントリでは Local::now を呼び出してローカル タイムゾーンの現在の DateTime を取得し、 DateTime::formatstrftime::specifiers を使って、 最終的なログで使用するタイムスタンプを整形します。

この例では Builder::format を呼び出して、各メッセージテキストを タイムスタンプ、Record::level、および本文(Record::args)で 整形するクロージャを設定しています。

use std::io::Write;
use chrono::Local;
use env_logger::Builder;
use log::LevelFilter;

fn main() {
    Builder::new()
        .format(|buf, record| {
            writeln!(buf,
                "{} [{}] - {}",
                Local::now().format("%Y-%m-%dT%H:%M:%S"),
                record.level(),
                record.args()
            )
        })
        .filter(None, LevelFilter::Info)
        .init();

    log::warn!("warn");
    log::info!("info");
    log::debug!("debug");
}

stderr の出力には次の内容が含まれます

2017-05-22T21:57:06 [WARN] - warn
2017-05-22T21:57:06 [INFO] - info

カスタムの場所にログメッセージを出力する

log-badge log4rs-badge cat-debugging-badge

log4rs はログ出力をカスタムの場所に設定します。log4rs は、外部 YAML ファイルまたはビルダー構成のいずれかを使用できます。

log4rs::append::file::FileAppender でログ設定を作成します。appender はログの出力先を定義します。設定は、log4rs::encode::pattern のカスタムパターンを使用したエンコードへと続きます。 その設定を log4rs::config::Config に割り当て、デフォルトの log::LevelFilter を設定します。

use anyhow::Result;
use log::LevelFilter;
use log4rs::append::file::FileAppender;
use log4rs::encode::pattern::PatternEncoder;
use log4rs::config::{Appender, Config, Root};

fn main() -> Result<()> {
    let logfile = FileAppender::builder()
        .encoder(Box::new(PatternEncoder::new("{l} - {m}\n")))
        .build("log/output.log")?;

    let config = Config::builder()
        .appender(Appender::builder().build("logfile", Box::new(logfile)))
        .build(Root::builder()
                   .appender("logfile")
                   .build(LevelFilter::Info))?;

    log4rs::init_config(config)?;

    log::info!("Hello, world!");

    Ok(())
}

トレース

tracing は、Rust プログラムに計装を行って 構造化されたイベントベースの診断情報を収集するためのフレームワークです。これは、従来の log クレートの代替であり、後方互換性を保つためのアダプターも備えています。

アプリケーションでトレースを有効にするには、次のクレートをプロジェクトに追加します:

cargo add tracing tracing-subscriber

ライブラリでは、通常 tracing-subscriber は必要ありません。

コンソールにログメッセージを出力する

tracing-badge tracing-subscriber-badge cat-debugging-badge

tracing クレートは、トレーシングサブスクライバーにログイベントを送出するためのマクロを提供します。 tracing-subscriber クレートは、イベントの送信先を設定します。デフォルトのトレーシング サブスクライバーをインストールするには、tracing_subscriber::fmt::init() を呼び出します。

use tracing::{debug, error, info, trace, warn};

fn main() {
    tracing_subscriber::fmt::init();

    error!("This is an error!");
    warn!("This is a warning.");
    info!("This is an informational message.");

    // with the default configuration, debug! and trace! messages are not shown
    debug!("This is a debug message.");
    trace!("This is a trace message.");
}

#[test]
fn test_main() {
    main();
}

デフォルトのログレベルは INFO です。Tracing は、それより低いレベルで記録されたイベントを破棄します。このコードを実行すると、 次の内容がコンソールに出力されます:

2024-12-01T07:56:14.778440Z ERROR tracing_console: これはエラーです!
2024-12-01T07:56:14.778568Z  WARN tracing_console: これは警告です。
2024-12-01T07:56:14.778596Z  INFO tracing_console: これは情報メッセージです。

より詳細なデフォルトレベルを設定するには、RUST_LOG 環境変数を設定します:

RUST_LOG=trace cargo run --example log-debug

Cargo は、上記の行に加えて次の追加の行を出力します:

2024-12-01T07:56:14.778613Z DEBUG tracing_console: これはデバッグメッセージです。
2024-12-01T07:56:14.778640Z TRACE tracing_console: これはトレースメッセージです。

バージョニング

バージョン文字列を解析してインクリメントする。

semver-badge cat-config-badge

Version::parse を使用して文字列リテラルから semver::Version を構築し、 その後、パッチ、マイナー、メジャーの各バージョン番号を順にインクリメントします。

Semantic Versioning Specification に従い、 マイナーのバージョン番号をインクリメントするとパッチのバージョン番号は 0 に リセットされ、メジャーのバージョン番号をインクリメントするとマイナーとパッチの バージョン番号の両方が 0 にリセットされることに注意してください。

use semver::{Version, Error as SemVerError};

fn main() -> Result<(), SemVerError> {
    let mut parsed_version = Version::parse("0.2.6")?;

    assert_eq!(
        parsed_version,
        Version::new(0, 2, 6)
    );

    parsed_version.patch += 1;
    assert_eq!(parsed_version.to_string(), "0.2.7");
    println!("New patch release: v{}", parsed_version);

    parsed_version.minor += 1;
    parsed_version.patch = 0;
    assert_eq!(parsed_version.to_string(), "0.3.0");
    println!("New minor release: v{}", parsed_version);

    parsed_version.major += 1;
    parsed_version.minor = 0;
    parsed_version.patch = 0;
    assert_eq!(parsed_version.to_string(), "1.0.0");
    println!("New major release: v{}", parsed_version);

    Ok(())
}

複雑なバージョン文字列を解析する。

semver-badge cat-config-badge

Version::parse を使用して、複雑なバージョン文字列から semver::Version を構築します。文字列 には、Semantic Versioning Specification で定義されているプレリリースとビルドメタデータが含まれます。

なお、仕様に従い、ビルドメタデータは解析されますが、バージョンを比較する際には 考慮されません。言い換えると、ビルド文字列が異なっていても、2 つのバージョンが等しい場合があります。

use semver::{Version, Prerelease, BuildMetadata, Error};

fn main() -> Result<(), Error> {
    let version_str = "1.0.49-125+g72ee7853";
    let parsed_version = Version::parse(version_str)?;

    assert_eq!(parsed_version.major, 1);
    assert_eq!(parsed_version.minor, 0);
    assert_eq!(parsed_version.patch, 49);
    assert_eq!(parsed_version.pre, Prerelease::new("125")?);
    assert_eq!(parsed_version.build, BuildMetadata::new("g72ee7853")?);

    let serialized_version = parsed_version.to_string();
    assert_eq!(&serialized_version, version_str);

    Ok(())
}

指定されたバージョンがプレリリースかどうかを確認する。

semver-badge cat-config-badge

2 つのバージョンが与えられたとき、[is_prerelease] は一方がプレリリースで、もう一方がそうでないことをアサートします。

use semver::{Version, Error};

fn main() -> Result<(), Error> {
    let version_1 = Version::parse("1.0.0-alpha")?;
    let version_2 = Version::parse("1.0.0")?;

    assert!(!version_1.pre.is_empty());
    assert!(version_2.pre.is_empty());

    Ok(())
}

指定した範囲を満たす最新バージョンを見つける

semver-badge cat-config-badge

バージョン &str のリストが与えられると、最新の semver::Version を見つけます。 semver::VersionReqVersionReq::matches を使ってリストを絞り込みます。 また、semver のプレリリースの優先順位も示しています。

use anyhow::Result;
use semver::{Version, VersionReq};

fn find_max_matching_version<'a, I>(version_req_str: &str, iterable: I) -> Result<Option<Version>>
where
    I: IntoIterator<Item = &'a str>,
{
    let vreq = VersionReq::parse(version_req_str)?;

    Ok(
        iterable
            .into_iter()
            .filter_map(|s| Version::parse(s).ok())
            .filter(|s| vreq.matches(s))
            .max(),
    )
}

fn main() -> Result<()> {
    assert_eq!(
        find_max_matching_version("<= 1.0.0", vec!["0.9.0", "1.0.0", "1.0.1"])?,
        Some(Version::parse("1.0.0")?)
    );

    assert_eq!(
        find_max_matching_version(
            ">1.2.3-alpha.3",
            vec![
                "1.2.3-alpha.3",
                "1.2.3-alpha.4",
                "1.2.3-alpha.10",
                "1.2.3-beta.4",
                "3.4.5-alpha.9",
            ]
        )?,
        Some(Version::parse("1.2.3-beta.4")?)
    );

    Ok(())
}

互換性を確認するために外部コマンドのバージョンをチェックする

semver-badge cat-text-processing-badge cat-os-badge

Command を使用して git --version を実行し、その後バージョン番号を Version::parse を使用して semver::Version に解析します。VersionReq::matchessemver::VersionReq を解析済みのバージョンと比較します。コマンドの出力は “git version x.y.z” のようになります。

use anyhow::{Result, anyhow};
use std::process::Command;
use semver::{Version, VersionReq};

fn main() -> Result<()> {
    let version_constraint = "> 1.12.0";
    let version_test = VersionReq::parse(version_constraint)?;
    let output = Command::new("git").arg("--version").output()?;

    if !output.status.success() {
        return Err(anyhow!("コマンドは失敗したエラーコードで実行されました"));
    }

    let stdout = String::from_utf8(output.stdout)?;
    let version = stdout.split(" ").last().ok_or_else(|| {
        anyhow!("無効なコマンド出力です")
    })?;
    let parsed_version = Version::parse(version)?;

    if !version_test.matches(&parsed_version) {
        return Err(anyhow!("コマンドのバージョンがサポートされる最小バージョンより低くなっています(検出: {}、必要: {})",
            parsed_version, version_constraint));
    }

    Ok(())
}

ビルド時ツール

このセクションでは、「ビルド時」のツール、つまりクレートのソースコードをコンパイルする前に実行されるコードを扱います。 慣例として、ビルド時のコードは build.rs ファイルに置かれ、一般に「ビルドスクリプト」と呼ばれます。 一般的なユースケースには、Rust のコード生成や、バンドルされた C/C++/asm コードのコンパイルがあります。 詳細については、この件に関する crates.io のドキュメントを参照してください。

バンドルされた C ライブラリを静的にコンパイルしてリンクする

cc-badge cat-development-tools-badge

プロジェクトで追加の C、C++、またはアセンブリが必要になるシナリオに対応するため、cc クレートは、バンドルされた C/C++/asm コードを静的ライブラリ(.a)にコンパイルするためのシンプルな API を提供しており、そのライブラリは rustc から静的リンクできます。

次の例には、Rust から使用するいくつかのバンドルされた C コード(src/hello.c)があります。 Rust のソースコードをコンパイルする前に、Cargo.toml で指定された「build」ファイル(build.rs)が実行されます。 cc クレートを使用すると、静的ライブラリファイルが生成されます(この場合は libhello.acompile docs を参照)。その後、extern ブロックで外部関数シグネチャを宣言することで、Rust からそれを使用できます。

このバンドルされた C は非常に単純なので、cc::Build に渡す必要があるソースファイルは 1 つだけです。 より複雑なビルド要件に対しては、cc::Buildinclude パスや追加のコンパイラ flag を指定するための、ひととおりのビルダーメソッドを提供しています。

Cargo.toml

[package]
...
build = "build.rs"

[build-dependencies]
cc = "1"

[dependencies]
anyhow = "1"

build.rs

fn main() {
    cc::Build::new()
        .file("src/hello.c")
        .compile("hello");   // `libhello.a` を出力する
}

src/hello.c

#include <stdio.h>


void hello() {
    printf("Hello from C!\n");
}

void greet(const char* name) {
    printf("Hello, %s!\n", name);
}

src/main.rs

use anyhow::Result;
use std::ffi::CString;
use std::os::raw::c_char;

fn prompt(s: &str) -> Result<String> {
    use std::io::Write;
    print!("{}", s);
    std::io::stdout().flush()?;
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    Ok(input.trim().to_string())
}

extern {
    fn hello();
    fn greet(name: *const c_char);
}

fn main() -> Result<()> {
    unsafe { hello() }
    let name = prompt("What's your name? ")?;
    let c_name = CString::new(name)?;
    unsafe { greet(c_name.as_ptr()) }
    Ok(())
}

バンドルされた C++ ライブラリを静的にコンパイルしてリンクする

cc-badge cat-development-tools-badge

バンドルされた C++ ライブラリのリンクは、バンドルされた C ライブラリのリンクと非常によく似ています。バンドルされた C++ ライブラリをコンパイルして静的にリンクする際の 2 つの主な違いは、ビルダーメソッド cpp(true) を使って C++ コンパイラを指定することと、C++ ソースファイルの先頭に extern "C" セクションを追加して C++ コンパイラによる名前修飾を防ぐことです。

Cargo.toml

[package]
...
build = "build.rs"

[build-dependencies]
cc = "1"

build.rs

fn main() {
    cc::Build::new()
        .cpp(true)
        .file("src/foo.cpp")
        .compile("foo");   
}

src/foo.cpp

extern "C" {
    int multiply(int x, int y);
}

int multiply(int x, int y) {
    return x*y;
}

src/main.rs

extern {
    fn multiply(x : i32, y : i32) -> i32;
}

fn main(){
    unsafe {
        println!("{}", multiply(5,7));
    }   
}

カスタム定義を設定しながら C ライブラリをコンパイルする

cc-badge cat-development-tools-badge

cc::Build::define を使うと、カスタム定義付きでバンドルされた C コードを簡単にビルドできます。 このメソッドは Option 値を受け取るため、#define APP_NAME "foo" のような定義や #define WELCOME のような定義も作成できます(値を持たない定義の場合は値として None を渡します)。この例では、 build.rs で設定した動的な定義を使ってバンドルされた C ファイルをビルドし、実行すると “Welcome to foo - version 1.0.2” と出力します。Cargo は、カスタム定義によっては役立つことがあるいくつかの 環境変数 を設定します。

Cargo.toml

[package]
...
version = "1.0.2"
build = "build.rs"

[build-dependencies]
cc = "1"

build.rs

fn main() {
    cc::Build::new()
        .define("APP_NAME", "\"foo\"")
        .define("VERSION", format!("\"{}\"", env!("CARGO_PKG_VERSION")).as_str())
        .define("WELCOME", None)
        .file("src/foo.c")
        .compile("foo");
}

src/foo.c

#include <stdio.h>

void print_app_info() {
#ifdef WELCOME
    printf("Welcome to ");
#endif
    printf("%s - version %s\n", APP_NAME, VERSION);
}

src/main.rs

extern {
    fn print_app_info();
}

fn main(){
    unsafe {
        print_app_info();
    }   
}

エンコーディング

文字セット

文字列をパーセントエンコードする

percent-encoding-badge cat-encoding-badge

入力文字列を、percent-encoding クレートの utf8_percent_encode 関数を使って percent-encoding します。次に、percent_decode 関数を使ってデコードします。

use percent_encoding::{utf8_percent_encode, percent_decode, AsciiSet, CONTROLS};
use std::str::Utf8Error;

/// https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');

fn main() -> Result<(), Utf8Error> {
    let input = "confident, productive systems programming";

    let iter = utf8_percent_encode(input, FRAGMENT);
    let encoded: String = iter.collect();
    assert_eq!(encoded, "confident,%20productive%20systems%20programming");

    let iter = percent_decode(encoded.as_bytes());
    let decoded = iter.decode_utf8()?;
    assert_eq!(decoded, "confident, productive systems programming");

    Ok(())
}

エンコードセットは、どのバイトを(非 ASCII 文字と制御文字に加えて)パーセントエンコードする必要があるかを定義します。このセットの選択はコンテキストに依存します。たとえば、url は URL パス内では ? をエンコードしますが、クエリ文字列内ではエンコードしません。

エンコードの戻り値は &str スライスのイテレータで、String に collect できます。

文字列を application/x-www-form-urlencoded としてエンコードする

url-badge cat-encoding-badge

文字列を form_urlencoded::byte_serialize を使って application/x-www-form-urlencoded 構文にエンコードし、その後 form_urlencoded::parse でデコードします。どちらの関数も String に collect できるイテレータを返します。

use url::form_urlencoded::{byte_serialize, parse};

fn main() {
    let urlencoded: String = byte_serialize("What is ❤?".as_bytes()).collect();
    assert_eq!(urlencoded, "What+is+%E2%9D%A4%3F");
    println!("urlencoded:'{}'", urlencoded);

    let decoded: String = parse(urlencoded.as_bytes())
        .map(|(key, val)| [key, val].concat())
        .collect();
    assert_eq!(decoded, "What is ❤?");
    println!("decoded:'{}'", decoded);
}

16進数のエンコードとデコード

data-encoding-badge cat-encoding-badge

data_encoding クレートは HEXUPPER::encode メソッドを提供しており、 これは &[u8] を受け取り、データの16進数表現を含む String を返します。

同様に、HEXUPPER::decode メソッドも提供されており、これは &[u8] を受け取り、 入力データのデコードに成功した場合は Vec<u8> を返します。

以下の例では、&[u8] データを対応する16進数表現に変換します。次に、この 値を期待される値と比較します。

use data_encoding::{HEXUPPER, DecodeError};

fn main() -> Result<(), DecodeError> {
    let original = b"The quick brown fox jumps over the lazy dog.";
    let expected = "54686520717569636B2062726F776E20666F78206A756D7073206F76\
        657220746865206C617A7920646F672E";

    let encoded = HEXUPPER.encode(original);
    assert_eq!(encoded, expected);

    let decoded = HEXUPPER.decode(&encoded.into_bytes())?;
    assert_eq!(&decoded[..], &original[..]);

    Ok(())
}

base64 をエンコードおよびデコードする

base64-badge cat-encoding-badge

encode を使用してバイトスライスを base64 の String にエンコードし、 decode でそれをデコードします。

use anyhow::Result;
use std::str;
use base64::prelude::{Engine as _, BASE64_STANDARD};

fn main() -> Result<()> {
    let hello = b"hello rustaceans";
    let encoded = BASE64_STANDARD.encode(hello);
    let decoded = BASE64_STANDARD.decode(&encoded)?;

    println!("origin: {}", str::from_utf8(hello)?);
    println!("base64 encoded: {}", encoded);
    println!("back to origin: {}", str::from_utf8(&decoded)?);

    Ok(())
}

CSV の処理

CSV レコードを読み取る

csv-badge cat-encoding-badge

標準的な CSV レコードを csv::StringRecord に読み込みます。これは、有効な UTF-8 の行を想定する、弱い型付けの データ表現です。あるいは、csv::ByteRecord は UTF-8 について何も仮定しません。

use csv::Error;

fn main() -> Result<(), Error> {
    let csv = "year,make,model,description
		1948,Porsche,356,Luxury sports car
		1967,Ford,Mustang fastback 1967,American car";

    let mut reader = csv::Reader::from_reader(csv.as_bytes());
    for record in reader.records() {
        let record = record?;
        println!(
            "In {}, {} built the {} model. It is a {}.",
            &record[0],
            &record[1],
            &record[2],
            &record[3]
        );
    }

    Ok(())
}

Serde はデータを強い型付けの構造体にデシリアライズします。csv::Reader::deserialize メソッドを参照してください。

use serde::Deserialize;
#[derive(Deserialize)]
struct Record {
    year: u16,
    make: String,
    model: String,
    description: String,
}

fn main() -> Result<(), csv::Error> {
    let csv = "year,make,model,description
1948,Porsche,356,Luxury sports car
1967,Ford,Mustang fastback 1967,American car";

    let mut reader = csv::Reader::from_reader(csv.as_bytes());

    for record in reader.deserialize() {
        let record: Record = record?;
        println!(
            "In {}, {} built the {} model. It is a {}.",
            record.year,
            record.make,
            record.model,
            record.description
        );
    }

    Ok(())
}

異なる区切り文字で CSV レコードを読み取る

csv-badge cat-encoding-badge

タブ [delimiter] を使用して CSV レコードを読み取ります。

use csv::Error;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Record {
    name: String,
    place: String,
    #[serde(deserialize_with = "csv::invalid_option")]
    id: Option<u64>,
}

use csv::ReaderBuilder;

fn main() -> Result<(), Error> {
    let data = "name\tplace\tid\n\
        Mark\tMelbourne\t46\n\
        Ashley\tZurich\t92";

    let mut reader = ReaderBuilder::new().delimiter(b'\t').from_reader(data.as_bytes());
    for result in reader.deserialize::<Record>() {
        println!("{:?}", result?);
    }

    Ok(())
}

述語に一致する CSV レコードをフィルタする

csv-badge cat-encoding-badge

query に一致するフィールドを持つ data の行 のみ を返します。

use anyhow::Result;
use std::io;

fn main() -> Result<()> {
    let query = "CA";
    let data = "\
City,State,Population,Latitude,Longitude
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111
Sandfort,AL,,32.3380556,-85.2233333
West Hollywood,CA,37031,34.0900000,-118.3608333";

    let mut rdr = csv::ReaderBuilder::new().from_reader(data.as_bytes());
    let mut wtr = csv::Writer::from_writer(io::stdout());

    wtr.write_record(rdr.headers()?)?;

    for result in rdr.records() {
        let record = result?;
        if record.iter().any(|field| field == query) {
            wtr.write_record(&record)?;
        }
    }

    wtr.flush()?;
    Ok(())
}

免責事項: この例は csv crate tutorial をもとに改変しています。

Serde で無効な CSV データを処理する

csv-badge serde-badge cat-encoding-badge

CSV ファイルには無効なデータが含まれていることがよくあります。このような場合に備えて、csv クレートはカスタムデシリアライザ csv::invalid_option を提供しており、無効なデータを自動的に None 値へ変換します。

use csv::Error;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Record {
    name: String,
    place: String,
    #[serde(deserialize_with = "csv::invalid_option")]
    id: Option<u64>,
}

fn main() -> Result<(), Error> {
    let data = "name,place,id
mark,sydney,46.5
ashley,zurich,92
akshat,delhi,37
alisha,colombo,xyz";

    let mut rdr = csv::Reader::from_reader(data.as_bytes());
    for result in rdr.deserialize() {
        let record: Record = result?;
        println!("{:?}", record);
    }

    Ok(())
}

レコードをCSVにシリアライズする

csv-badge cat-encoding-badge

この例では、Rust のタプルをシリアライズする方法を示します。csv::writer は、Rust の型から CSV レコードへの自動シリアライズをサポートしています。write_record は、文字列データのみを含む単純なレコードを書き込みます。数値、浮動小数点数、オプションなどのより複雑な値を持つデータには serialize を使用します。CSV writer は内部バッファを使用するため、完了したら必ず明示的に flush してください。

use anyhow::Result;
use std::io;

fn main() -> Result<()> {
    let mut wtr = csv::Writer::from_writer(io::stdout());

    wtr.write_record(&["Name", "Place", "ID"])?;

    wtr.serialize(("Mark", "Sydney", 87))?;
    wtr.serialize(("Ashley", "Dublin", 32))?;
    wtr.serialize(("Akshat", "Delhi", 11))?;

    wtr.flush()?;
    Ok(())
}

Serde を使用してレコードを CSV にシリアライズする

csv-badge serde-badge cat-encoding-badge

次の例は、serde クレートを使用してカスタム構造体を CSV レコードとして シリアライズする方法を示しています。

use anyhow::Result;
use serde::Serialize;
use std::io;

#[derive(Serialize)]
struct Record<'a> {
    name: &'a str,
    place: &'a str,
    id: u64,
}

fn main() -> Result<()> {
    let mut wtr = csv::Writer::from_writer(io::stdout());

    let rec1 = Record { name: "Mark", place: "Melbourne", id: 56};
    let rec2 = Record { name: "Ashley", place: "Sydney", id: 64};
    let rec3 = Record { name: "Akshat", place: "Delhi", id: 98};

    wtr.serialize(rec1)?;
    wtr.serialize(rec2)?;
    wtr.serialize(rec3)?;

    wtr.flush()?;

    Ok(())
}

CSV 列を変換する

csv-badge serde-badge cat-encoding-badge

色名と 16 進カラーを含む CSV ファイルを、色名と RGB カラーを含む CSV ファイルに変換します。csv クレートを使用して CSV ファイルの読み書きを行い、 serde を使用して行をバイト列との間でデシリアライズおよびシリアライズします。

csv::Reader::deserializeserde::Deserializestd::str::FromStr を参照してください

use anyhow::{Result, anyhow};
use csv::{Reader, Writer};
use serde::{de, Deserialize, Deserializer};
use std::str::FromStr;

#[derive(Debug)]
struct HexColor {
    red: u8,
    green: u8,
    blue: u8,
}

#[derive(Debug, Deserialize)]
struct Row {
    color_name: String,
    color: HexColor,
}

impl FromStr for HexColor {
    type Err = anyhow::Error;

    fn from_str(hex_color: &str) -> std::result::Result<Self, Self::Err> {
        let trimmed = hex_color.trim_matches('#');
        if trimmed.len() != 6 {
            Err(anyhow!("Invalid length of hex string"))
        } else {
            Ok(HexColor {
                red: u8::from_str_radix(&trimmed[..2], 16)?,
                green: u8::from_str_radix(&trimmed[2..4], 16)?,
                blue: u8::from_str_radix(&trimmed[4..6], 16)?,
            })
        }
    }
}

impl<'de> Deserialize<'de> for HexColor {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        FromStr::from_str(&s).map_err(de::Error::custom)
    }
}

fn main() -> Result<()> {
    let data = "color_name,color
red,#ff0000
green,#00ff00
blue,#0000FF
periwinkle,#ccccff
magenta,#ff00ff"
        .to_owned();
    let mut out = Writer::from_writer(vec![]);
    let mut reader = Reader::from_reader(data.as_bytes());
    for result in reader.deserialize::<Row>() {
        let res = result?;
        out.serialize((
            res.color_name,
            res.color.red,
            res.color.green,
            res.color.blue,
        ))?;
    }
    let written = String::from_utf8(out.into_inner()?)?;
    assert_eq!(Some("magenta,255,0,255"), written.lines().last());
    println!("{}", written);
    Ok(())
}

構造化データ

非構造化 JSON をシリアライズおよびデシリアライズする

serde-json-badge cat-encoding-badge

serde_json クレートは、JSON の &str をパースするための from_str 関数を提供します。

非構造化 JSON は、任意の有効な JSON データを表現できる汎用的な serde_json::Value 型にパースできます。

以下の例では、JSON の &str をパースしています。 期待される値は json! マクロを使って宣言しています。

use serde_json::json;
use serde_json::{Value, Error};

fn main() -> Result<(), Error> {
    let j = r#"{
                 "userid": 103609,
                 "verified": true,
                 "access_privileges": [
                   "user",
                   "admin"
                 ]
               }"#;

    let parsed: Value = serde_json::from_str(j)?;

    let expected = json!({
        "userid": 103609,
        "verified": true,
        "access_privileges": [
            "user",
            "admin"
        ]
    });

    assert_eq!(parsed, expected);

    Ok(())
}

TOML 設定ファイルをデシリアライズする

toml-badge cat-encoding-badge

任意の有効な TOML データを表現できる汎用的な toml::Value に、TOML をパースします。

use toml::{Value, de::Error};

fn main() -> Result<(), Error> {
    let toml_content = r#"
          [package]
          name = "your_package"
          version = "0.1.0"
          authors = ["You! <you@example.org>"]

          [dependencies]
          serde = "1.0"
          "#;

    let package_info: Value = toml::from_str(toml_content)?;

    assert_eq!(package_info["dependencies"]["serde"].as_str(), Some("1.0"));
    assert_eq!(package_info["package"]["name"].as_str(),
               Some("your_package"));

    Ok(())
}

Serde を使って、TOML を独自の構造体にパースします。

use serde::Deserialize;

use toml::de::Error;
use std::collections::HashMap;

#[derive(Deserialize)]
struct Config {
    package: Package,
    dependencies: HashMap<String, String>,
}

#[derive(Deserialize)]
struct Package {
    name: String,
    version: String,
    authors: Vec<String>,
}

fn main() -> Result<(), Error> {
    let toml_content = r#"
          [package]
          name = "your_package"
          version = "0.1.0"
          authors = ["You! <you@example.org>"]

          [dependencies]
          serde = "1.0"
          "#;

    let package_info: Config = toml::from_str(toml_content)?;

    assert_eq!(package_info.package.name, "your_package");
    assert_eq!(package_info.package.version, "0.1.0");
    assert_eq!(package_info.package.authors, vec!["You! <you@example.org>"]);
    assert_eq!(package_info.dependencies["serde"], "1.0");

    Ok(())
}

リトルエンディアンのバイト順で整数を読み書きする

byteorder-badge cat-encoding-badge

byteorder は、構造化データの有効バイトを逆順にできます。これは、ネットワーク経由で情報を受信する際に、受信したバイトが別のシステムから送られてくる場合などに必要になることがあります。

use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::io::Error;

#[derive(Default, PartialEq, Debug)]
struct Payload {
    kind: u8,
    value: u16,
}

fn main() -> Result<(), Error> {
    let original_payload = Payload::default();
    let encoded_bytes = encode(&original_payload)?;
    let decoded_payload = decode(&encoded_bytes)?;
    assert_eq!(original_payload, decoded_payload);
    Ok(())
}

fn encode(payload: &Payload) -> Result<Vec<u8>, Error> {
    let mut bytes = vec![];
    bytes.write_u8(payload.kind)?;
    bytes.write_u16::<LittleEndian>(payload.value)?;
    Ok(bytes)
}

fn decode(mut bytes: &[u8]) -> Result<Payload, Error> {
    let payload = Payload {
        kind: bytes.read_u8()?,
        value: bytes.read_u16::<LittleEndian>()?,
    };
    Ok(payload)
}

エラー処理

エラー処理

mainでエラーを正しく処理する

anyhow-badge cat-rust-patterns-badge thiserror-badge cat-rust-patterns-badge error-chain-badge cat-rust-patterns-badge

エラー戦略 (2024)

Rust by Example で推奨されているように、Boxing errors は使い始めるための 簡単な戦略と見なされています。

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    // エラーを Box 化する例
    let result: Result<(), Box<dyn Error>> = Ok(());
    result
}

どのような種類のエラー処理が必要になる可能性があるかを理解するには、Designing error types in Rust を読み、ライブラリには thiserror を、保守されている エラー集約の選択肢として anyhow を検討してください。

use thiserror::Error;

#[derive(Error,Debug)]
pub enum MultiError {
  #[error("🦀 got {0}")]
  ErrorClass(String),
}

fn main() -> Result<(), MultiError> {
    // thiserror を使用する例
    Ok(())
}

アプリケーション作成者は anyhow を使って列挙型を構成でき、クレートから Result 型をインポートすることで自動 Box 化の挙動を利用できます

use anyhow::Result;

fn main() -> Result<(), Box<dyn std::error::Error>> {
   let my_string = "yellow".to_string();  
   let _my_int = my_string.parse::<i32>()?;
   Ok(())
}

Error Chain (2015-2018)

存在しないファイルを開こうとしたときに発生するエラーを処理します。これは、 handle errors in Rust ために必要となる多くのボイラープレートコードを担ってくれる ライブラリ error-chain を使うことで実現されます。

foreign_links 内の Io(std::io::Error) により、自動的に std::io::Error から error_chain! で定義された型へ変換でき、 その型は Error トレイトを実装します。

以下のレシピは、Unix ファイル /proc/uptime を開いて内容を解析し、最初の 数値を取得することで、システムがどれだけ長く稼働しているかを示します。 エラーがない限り、稼働時間を返します。

この本の他のレシピでは error-chain のボイラープレートは隠されており、⤢ ボタンで コードを展開すると確認できます。

use error_chain::error_chain;

use std::fs::File;
use std::io::Read;

error_chain!{
    foreign_links {
        Io(std::io::Error);
        ParseInt(::std::num::ParseIntError);
    }
}

fn read_uptime() -> Result<u64> {
    let mut uptime = String::new();
    File::open("/proc/uptime")?.read_to_string(&mut uptime)?;

    Ok(uptime
        .split('.')
        .next()
        .ok_or("Cannot parse uptime data")?
        .parse()?)
}

fn main() {
    match read_uptime() {
        Ok(uptime) => println!("uptime: {} seconds", uptime),
        Err(err) => eprintln!("error: {}", err),
    };
}

ファイルシステム

読み取りと書き込み

ファイルから文字列の行を読み込む

std-badge cat-filesystem-badge

3行のメッセージをファイルに書き込み、その後、 BufRead::lines によって作成される Lines イテレータを使って、 1行ずつ読み戻します。FileRead を実装しており、それにより BufReader トレイトが提供されます。File::create は書き込み用に File を開き、File::open は読み込み用に開きます。

use std::fs::File;
use std::io::{Write, BufReader, BufRead, Error};

fn main() -> Result<(), Error> {
    let path = "lines.txt";

    let mut output = File::create(path)?;
    write!(output, "Rust\n💖\nFun")?;

    let input = File::open(path)?;
    let buffered = BufReader::new(input);

    for line in buffered.lines() {
        println!("{}", line?);
    }

    Ok(())
}

同じファイルの読み書きを避ける

same_file-badge cat-filesystem-badge

他のハンドルと等しいかどうかをテストできるファイルには same_file::Handle を使用します。この例では、読み込み元のファイルと 書き込み先のファイルのハンドルが等しいかどうかをテストしています。

use same_file::Handle;
use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
use std::fs::File;
use std::path::Path;

fn main() -> Result<(), Error> {
    // テストファイルを作成する
    let mut file = File::create("new.txt")?;
    writeln!(file, "test content")?;
    
    let path_to_read = Path::new("new.txt");

    let stdout_handle = Handle::stdout()?;
    let handle = Handle::from_path(path_to_read)?;

    if stdout_handle == handle {
        return Err(Error::new(
            ErrorKind::Other,
            "同じファイルを読み書きしています",
        ));
    } else {
        let file = File::open(&path_to_read)?;
        let file = BufReader::new(file);
        for (num, line) in file.lines().enumerate() {
            println!("{} : {}", num, line?.to_uppercase());
        }
    }
    Ok(())
}

メモリマップを使ってファイルにランダムアクセスする

memmap-badge cat-filesystem-badge

memmap を使用してファイルのメモリマップを作成し、ファイルに対するいくつかの非連続な読み取りをシミュレートします。メモリマップを使うと、File 内で seek を繰り返し行う代わりに、単にスライスにインデックスを付けるだけで済みます。

Mmap::map 関数は、メモリマップの対象となるファイルが同時に別のプロセスによって変更されていないことを前提としています。そうでない場合、race condition が発生します。

use memmap::Mmap;
use std::fs::File;
use std::io::{Write, Error};

fn main() -> Result<(), Error> {
    write!(File::create("content.txt")?, "My hovercraft is full of eels!")?;

    let file = File::open("content.txt")?;
    let map = unsafe { Mmap::map(&file)? };

    let random_indexes = [0, 1, 2, 19, 22, 10, 11, 29];
    assert_eq!(&map[3..13], b"hovercraft");
    let random_bytes: Vec<u8> = random_indexes.iter()
        .map(|&idx| map[idx])
        .collect();
    assert_eq!(&random_bytes[..], b"My loaf!");
    Ok(())
}

ディレクトリの走査

過去24時間以内に変更されたファイル名

walkdir-badge cat-filesystem-badge

現在の作業ディレクトリを取得し、過去24時間以内に変更されたファイル名を返します。 env::current_dir は現在の作業ディレクトリを取得し、WalkDir::new は現在のディレクトリに対する新しい WalkDir を作成します。 WalkDir::into_iter はイテレータを作成し、Iterator::filter_mapWalkDir::DirEntryResult::ok を適用してディレクトリを除外します。

std::fs::Metadata::modified は、最後の変更からの SystemTime::elapsed 時間を返します。 Duration::as_secs はその時間を秒に変換し、24時間(24 * 60 * 60 秒)と比較します。 Iterator::for_each はファイル名を出力します。

use walkdir::WalkDir;
use anyhow::Result;
use std::env;

fn main() -> Result<()> {
    let current_dir = env::current_dir()?;
    println!("Entries modified in the last 24 hours in {:?}:", current_dir);

    for entry in WalkDir::new(current_dir)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.metadata().unwrap().is_file()) {
        let path = entry.path();
        let metadata = entry.metadata()?;
        let modified = metadata.modified()?.elapsed()?.as_secs();
        if modified < 24 * 3600 {
            println!("{}", path.display());
        }
    }

    Ok(())
}

指定されたパスのループを見つける

same_file-badge walkdir-badge cat-filesystem-badge

指定されたパスのループを検出するには same_file::is_same_file を使用します。 たとえば、Unix システムではシンボリックリンクによってループが作成されます:

mkdir -p /tmp/foo/bar/baz
ln -s /tmp/foo/  /tmp/foo/bar/baz/qux

次のコードは、ループが存在することを確認します。

use walkdir::WalkDir;
use same_file::is_same_file;

fn main() {
    let mut loop_found = false;
    for entry in WalkDir::new(".")
        .follow_links(true)
        .into_iter()
        .filter_map(|e| e.ok()) {
        let ancestor = entry.path()
            .ancestors()
            .skip(1)
            .find(|ancestor| is_same_file(ancestor, entry.path()).is_ok());

        if ancestor.is_some() {
            loop_found = true;
        }
    }
    // 注: このテストは、実際にシンボリックリンクのループがある場合にのみ成功します
    // println!("Loop found: {}", loop_found);
}

重複するファイル名を再帰的に検索する

walkdir-badge cat-filesystem-badge

現在のディレクトリ内で重複するファイル名を再帰的に検索し、 それぞれを1回だけ出力します。

use walkdir::WalkDir;
use std::collections::HashMap;

fn main() {
    let mut filenames = HashMap::new();

    for entry in WalkDir::new(".")
                         .into_iter()
                         .filter_map(Result::ok)
                         .filter(|e| e.file_type().is_file()) {

        let f_name = String::from(entry.file_name().to_string_lossy());
        let counter = filenames.entry(f_name.clone()).or_insert(0);
        *counter += 1;

        if *counter == 2 {
            println!("{}", f_name);
        }
    }
}

指定した述語に一致するすべてのファイルを再帰的に検索する

walkdir-badge cat-filesystem-badge

現在のディレクトリ内で、過去1日以内に変更された JSON ファイルを見つけます。 follow_links を使用すると、シンボリックリンクが通常のディレクトリやファイルであるかのように たどられることが保証されます。

use walkdir::WalkDir;
use anyhow::Result;

fn main() -> Result<()> {
    for entry in WalkDir::new(".")
        .follow_links(true)
        .into_iter()
        .filter_map(|e| e.ok()) {
        let f_name = entry.file_name().to_string_lossy();
        let sec = entry.metadata()?.modified()?;

        if f_name.ends_with(".json") && sec.elapsed()?.as_secs() < 86400 {
            println!("{}", entry.path().display());
        }
    }
    Ok(())
}

ドットファイルをスキップしながらディレクトリをたどる

walkdir-badge cat-filesystem-badge

filter_entry を使用して、is_not_hidden 述語を満たすエントリに対して再帰的に降下し、隠しファイルと隠しディレクトリをスキップします。 Iterator::filter_map は、親が隠しディレクトリであっても、各 WalkDir::DirEntryis_not_hidden を適用します。

ルートディレクトリ "." は、is_not_hidden 述語で WalkDir::depth を使用しているため、対象に含まれます。

use walkdir::{DirEntry, WalkDir};

fn is_not_hidden(entry: &DirEntry) -> bool {
    entry
         .file_name()
         .to_str()
         .map(|s| entry.depth() == 0 || !s.starts_with("."))
         .unwrap_or(false)
}

fn main() {
    WalkDir::new(".")
        .into_iter()
        .filter_entry(|e| is_not_hidden(e))
        .filter_map(|v| v.ok())
        .for_each(|x| println!("{}", x.path().display()));
}

指定した深さでファイルサイズを再帰的に計算する

walkdir-badge cat-filesystem-badge

再帰の深さは WalkDir::max_depth で柔軟に設定できます。ルートディレクトリ内のファイルを無視し、3 レベル下のサブディレクトリまでにあるすべてのファイルサイズの合計を計算します。

use walkdir::WalkDir;

fn main() {
    let total_size = WalkDir::new(".")
        .max_depth(3)
        .into_iter()
        .filter_map(|entry| entry.ok())
        .filter_map(|entry| entry.metadata().ok())
        .filter(|metadata| metadata.is_file())
        .fold(0, |acc, m| acc + m.len());

    println!("Total size: {} bytes.", total_size);
}

すべての PNG ファイルを再帰的に検索する

glob-badge cat-filesystem-badge

現在のディレクトリ内にあるすべての PNG ファイルを再帰的に検索します。 この場合、** パターンは現在のディレクトリとそのすべてのサブディレクトリにマッチします。

パスの任意の部分で ** パターンを使用できます。たとえば、/media/**/*.pngmedia とそのサブディレクトリ内のすべての PNG にマッチします。

use glob::glob;
use anyhow::Result;

fn main() -> Result<()> {
    for entry in glob("**/*.png")? {
        println!("{}", entry?.display());
    }
    Ok(())
}

ファイル名の大文字と小文字を無視して、指定したパターンに一致するすべてのファイルを検索する

walkdir-badge glob-badge cat-filesystem-badge

/media/ ディレクトリ内で、img_[0-9]*.png パターンに一致するすべての画像ファイルを検索します。

他のオプションは Default のままにしつつ、glob パターンで大文字と小文字を区別しないようにするため、glob の代わりにカスタムの MatchOptions 構造体を glob_with に渡します。

use walkdir::WalkDir;
use anyhow::Result;
use glob::{glob_with, MatchOptions};

fn main() -> Result<()> {
    let options = MatchOptions {
        case_sensitive: false,
        ..Default::default()
    };

    for entry in glob_with("/media/img_[0-9]*.png", options)? {
        println!("{}", entry?.display());
    }

    Ok(())
}

ハードウェアサポート

レシピクレートカテゴリ
論理 CPU コア数を確認するnum_cpus-badgecat-hardware-support-badge

プロセッサ

論理 CPU コア数を確認する

num_cpus-badge cat-hardware-support-badge

num_cpus::get を使用して、現在のマシンの論理 CPU コア数を表示します。

fn main() {
    println!("Number of logical cores is {}", num_cpus::get());
}

メモリ管理

定数

遅延評価される定数を宣言する

lazy_static-badge cat-caching-badge cat-rust-patterns-badge

遅延評価される定数 HashMap を宣言します。HashMap は 一度だけ評価され、グローバルな static 参照を介して保存されます。

use lazy_static::lazy_static;
use std::collections::HashMap;

lazy_static! {
    static ref PRIVILEGES: HashMap<&'static str, Vec<&'static str>> = {
        let mut map = HashMap::new();
        map.insert("James", vec!["user", "admin"]);
        map.insert("Jim", vec!["user"]);
        map
    };
}

fn show_access(name: &str) {
    let access = PRIVILEGES.get(name);
    println!("{}: {:?}", name, access);
}

fn main() {
    let access = PRIVILEGES.get("James");
    println!("James: {:?}", access);

    show_access("Jim");
}

Std:cell

OnceCell は代替手段として標準ライブラリに含まれています。

use std::cell::OnceCell;

fn main() {
    let cell = OnceCell::new();
    assert!(cell.get().is_none());

    let value: &String = cell.get_or_init(|| {
        "Hello, World!".to_string()
    });
    assert_eq!(value, "Hello, World!");
    assert!(cell.get().is_some());
}

std::cell::LazyCell

std-badge cat-caching-badge cat-rust-patterns-badge

LazyCell 型は、値への初回アクセス時までその初期化を遅らせるために使用できます。 これは、スコープの実行全体を通して必要にならない可能性がある高コストな計算に役立ちます。

この例では、LazyCell を使って、ユーザー ID に基づく権限セットを遅延計算します。 static な遅延型とは異なり、LazyCell は周囲のスコープからローカル変数をキャプチャできます。

use std::cell::LazyCell;

fn main() {
    let user_id = 42;

    // クロージャはまだ実行されません。
    let permissions = LazyCell::new(|| {
        println!("--- Fetching permissions from database for ID {} ---", user_id);
        // 高コストな操作をシミュレートする
        vec!["read".to_string(), "write".to_string()]
    });

    println!("User {} session started.", user_id);

    // 初期化は、permissions を初めてデリファレンスしたときにのみ行われます。
    if true { // ここに条件チェックがあると想像してください
        println!("Permissions: {:?}", *permissions);
    }
    
    // 以降のアクセスでは、すでに初期化済みの値が使用されます。
    println!("First permission: {}", permissions[0]);
}

std::sync::LazyLock

std-badge cat-caching-badge cat-rust-patterns-badge

LazyLock 型は、static コンテキストで使用できる LazyCell のスレッドセーフ版です。 これは、グローバルで遅延初期化されるデータに対する lazy_static クレートの標準ライブラリ版の代替手段です。

この例では、初回アクセス時に環境から一度だけ読み込まれる グローバルな設定を作成するために LazyLock をどのように使用するかを示します。

use std::sync::LazyLock;

struct Config {
    api_key: String,
    timeout: u64,
}

// 最初の使用時に初期化されるグローバル設定。
static APP_CONFIG: LazyLock<Config> = LazyLock::new(|| {
    println!("Loading configuration...");
    Config {
        api_key: std::env::var("API_KEY").unwrap_or_else(|_| "default_key".to_string()),
        timeout: 30,
    }
});

fn main() {
    println!("App started.");

    // 初期化はここで行われます。
    let timeout = APP_CONFIG.timeout;

    println!("Timeout is: {}s", timeout);
    println!("API Key is hidden: {}", APP_CONFIG.api_key.len() > 0);
}

ネットワーキング

レシピクレートカテゴリー
未使用のポートで TCP/IP を待ち受けるstd-badgecat-net-badge

サーバー

未使用の TCP/IP ポートで待ち受ける

std-badge cat-net-badge

この例では、ポート番号がコンソールに表示され、プログラムは 要求があるまで待ち受けます。TcpListener::bind は、ポート 0 への バインドが要求された場合、OS によって割り当てられたランダムなポートを使用します。

use std::net::TcpListener;
use std::io::{Read, Error};

fn main() -> Result<(), Error> {
    let listener = TcpListener::bind("localhost:0")?;
    let port = listener.local_addr()?;
    println!("{} で待ち受けています。プログラムを終了するにはこのポートにアクセスしてください", port);
    let (mut tcp_stream, addr) = listener.accept()?; //要求があるまでブロックする
    println!("接続を受信しました。{:?} がデータを送信しています。", addr);
    let mut input = String::new();
    let _ = tcp_stream.read_to_string(&mut input)?;
    println!("{:?} が {} と言っています", addr, input);
    Ok(())
}

オペレーティングシステム

外部コマンド

外部コマンドを実行して stdout を処理する

std-badge cat-os-badge

外部の Command を使って git log --oneline を実行し、Output の status を調べてコマンドが成功したかどうかを判定します。コマンドの出力は [String::from_utf8] を使って [String] として取得します。

use anyhow::{Result, anyhow};
use std::process::Command;

fn main() -> Result<()> {
    let output = Command::new("git").arg("log").arg("--oneline").output()?;

    if output.status.success() {
        let raw_output = String::from_utf8(output.stdout)?;
        let lines = raw_output.lines();
        println!("Found {} lines", lines.count());
        Ok(())
    } else {
        return Err(anyhow!("Command executed with failing error code"));
    }
}

stdin を渡して外部コマンドを実行し、エラーコードを確認する

std-badge cat-os-badge

外部の Command を使って python インタープリタを起動し、実行するための Python 文を渡します。その文の Output をその後で解析します。

use anyhow::{Result, anyhow};
use std::collections::HashSet;
use std::io::Write;
use std::process::{Command, Stdio};

fn main() -> Result<()> {
    let mut child = Command::new("python").stdin(Stdio::piped())
        .stderr(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;

    child.stdin
        .as_mut()
        .ok_or_else(|| anyhow!("Child process stdin has not been captured!"))?
        .write_all(b"import this; copyright(); credits(); exit()")?;

    let output = child.wait_with_output()?;

    if output.status.success() {
        let raw_output = String::from_utf8(output.stdout)?;
        let words = raw_output.split_whitespace()
            .map(|s| s.to_lowercase())
            .collect::<HashSet<_>>();
        println!("Found {} unique words:", words.len());
        println!("{:#?}", words);
        Ok(())
    } else {
        let err = String::from_utf8(output.stderr)?;
        return Err(anyhow!("External command failed:\n {}", err));
    }
}

パイプでつないだ外部コマンドを実行する

std-badge cat-os-badge

現在の作業ディレクトリ内で、サイズが大きい上位 10 個のファイルとサブディレクトリを表示します。これは次を実行するのと同等です: du -ah . | sort -hr | head -n 10

Command はプロセスを表します。子プロセスの出力は、親プロセスと子プロセスの間の Stdio::piped でキャプチャされます。

use anyhow::Result;
use std::process::{Command, Stdio};

fn main() -> Result<()> {
    let directory = std::env::current_dir()?;
    let mut du_output_child = Command::new("du")
        .arg("-ah")
        .arg(&directory)
        .stdout(Stdio::piped())
        .spawn()?;

    if let Some(du_output) = du_output_child.stdout.take() {
        let mut sort_output_child = Command::new("sort")
            .arg("-hr")
            .stdin(du_output)
            .stdout(Stdio::piped())
            .spawn()?;

        du_output_child.wait()?;

        if let Some(sort_output) = sort_output_child.stdout.take() {
            let head_output_child = Command::new("head")
                .args(&["-n", "10"])
                .stdin(sort_output)
                .stdout(Stdio::piped())
                .spawn()?;

            let head_stdout = head_output_child.wait_with_output()?;

            sort_output_child.wait()?;

            println!(
                "Top 10 biggest files and directories in '{}':\n{}",
                directory.display(),
                String::from_utf8(head_stdout.stdout).unwrap()
            );
        }
    }

    Ok(())
}

子プロセスの stdout と stderr の両方を同じファイルにリダイレクトする

std-badge cat-os-badge

子プロセスを生成し、stdoutstderr を同じファイルにリダイレクトします。パイプで接続した外部コマンドを実行する と同じ考え方ですが、process::Stdio は指定したファイルに書き込みます。File::try_clonestdoutstderr に対して同じファイルハンドルを参照します。これにより、両方のハンドルが同じカーソル位置で書き込むことが保証されます。

以下のレシピは、Unix シェルコマンド ls . oops >out.txt 2>&1 を実行するのと同等です。

use std::fs::File;
use std::io::Error;
use std::process::{Command, Stdio};

fn main() -> Result<(), Error> {
    let outputs = File::create("out.txt")?;
    let errors = outputs.try_clone()?;

    Command::new("ls")
        .args(&[".", "oops"])
        .stdout(Stdio::from(outputs))
        .stderr(Stdio::from(errors))
        .spawn()?
        .wait_with_output()?;

    Ok(())
}

子プロセスの出力を継続的に処理する

std-badge cat-os-badge

外部コマンドを実行して stdout を処理する では、 外部 Command が終了するまで処理は開始されません。 以下のレシピでは Stdio::piped を呼び出してパイプを作成し、 BufReader が更新されるたびに stdout を継続的に読み取ります。

以下のレシピは、Unix シェルコマンド journalctl | grep usb と等価です。

use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader, Error, ErrorKind};

fn main() -> Result<(), Error> {
    let stdout = Command::new("journalctl")
        .stdout(Stdio::piped())
        .spawn()?
        .stdout
        .ok_or_else(|| Error::new(ErrorKind::Other,"Could not capture standard output."))?;

    let reader = BufReader::new(stdout);

    reader
        .lines()
        .filter_map(|line| line.ok())
        .filter(|line| line.find("usb").is_some())
        .for_each(|line| println!("{}", line));

     Ok(())
}

環境変数を読み取る

std-badge cat-os-badge

std::env::var を介して環境変数を読み取ります。

use std::env;
use std::fs;
use std::io::Error;

fn main() -> Result<(), Error> {
    // 環境変数 `CONFIG` から `config_path` を読み取る。
    // `CONFIG` が設定されていない場合は、デフォルトの設定ファイルパスにフォールバックする。
    let config_path = env::var("CONFIG")
        .unwrap_or("/etc/myapp/config".to_string());

    let config: String = fs::read_to_string(config_path)?;
    println!("Config: {}", config);

    Ok(())
}

科学

science/mathematics

数学

数学

線形代数

行列の加算

ndarray-badge cat-science-badge

ndarray::arr2 を使用して 2 つの 2 次元行列を作成し、要素ごとに加算します。

和は let sum = &a + &b として計算されることに注意してください。& 演算子は ab を消費しないようにするために使用され、後で表示できるようになります。それらの和を含む新しい配列が作成されます。


use ndarray::arr2;

fn main() {
    let a = arr2(&[[1, 2, 3],
                   [4, 5, 6]]);

    let b = arr2(&[[6, 5, 4],
                   [3, 2, 1]]);

    let sum = &a + &b;

    println!("{}", a);
    println!("+");
    println!("{}", b);
    println!("=");
    println!("{}", sum);
}

行列の乗算

ndarray-badge cat-science-badge

ndarray::arr2 で 2 つの行列を作成し、ndarray::ArrayBase::dot でそれらの行列乗算を実行します。


use ndarray::arr2;

fn main() {
    let a = arr2(&[[1, 2, 3],
                   [4, 5, 6]]);

    let b = arr2(&[[6, 3],
                   [5, 2],
                   [4, 1]]);

    println!("{}", a.dot(&b));
}

スカラーをベクトルに掛け、さらに行列を掛ける

ndarray-badge cat-science-badge

ndarray::arr1 で 1 次元配列(ベクトル)を作成し、ndarray::arr2 で 2 次元配列(行列)を作成します。

まず、スカラーをベクトルに掛けて別のベクトルを得ます。次に、その新しいベクトルに 対して行列を ndarray::Array2::dot で掛けます。(行列の乗算は dot を使用して行われます。一方、 * 演算子は要素ごとの乗算を行います。)

ndarray では、1 次元配列は文脈に応じて行ベクトルまたは列ベクトルの いずれとしても解釈できます。ベクトルの向きを表すことが重要な場合は、 代わりに 1 行または 1 列の 2 次元配列を使用する必要があります。この例では、 ベクトルは右辺の 1 次元配列であるため、dot はそれを列ベクトルとして 扱います。


use ndarray::{arr1, arr2, Array1};

fn main() {
    let scalar = 4;

    let vector = arr1(&[1, 2, 3]);

    let matrix = arr2(&[[4, 5, 6],
                        [7, 8, 9]]);

    let new_vector: Array1<_> = scalar * vector;
    println!("{}", new_vector);

    let new_matrix = matrix.dot(&new_vector);
    println!("{}", new_matrix);
}

ベクトルの比較

ndarray-badge

ndarray クレートは配列を作成するためのさまざまな方法をサポートしています。このレシピでは、from を使って std::Vec から ndarray::Array を作成します。次に、それらの配列を要素ごとに加算します。

このレシピには、2 つの浮動小数点ベクトルを要素ごとに比較する例も含まれています。 単純なケースでは、厳密な等価比較に assert_eq! を使用できます。より 複雑な浮動小数点比較で精度の問題を扱う必要がある場合は、Cargo.tomlndarray 依存関係で approx 機能を有効にしたうえで approx クレートを使用できます。たとえば、 ndarray = { version = "0.13", features = ["approx"] } のようにします。

このレシピには、所有権に関する追加の例も含まれています。ここでは、let z = a + b によって ab が消費され、a が結果で更新された後、その所有権が z にムーブされます。別の方法として、 let w = &c + &dcd を消費せずに新しいベクトルを作成するため、 後からそれらを変更できます。詳細については Binary Operators With Two Arrays を参照してください。


use ndarray::Array;

fn main() {
  let a = Array::from(vec![1., 2., 3., 4., 5.]);
  let b = Array::from(vec![5., 4., 3., 2., 1.]);
  let mut c = Array::from(vec![1., 2., 3., 4., 5.]);
  let mut d = Array::from(vec![5., 4., 3., 2., 1.]);

  let z = a + b;
  let w =  &c + &d;

  assert_eq!(z, Array::from(vec![6., 6., 6., 6., 6.]));

  println!("c = {}", c);
  c[0] = 10.;
  d[1] = 10.;

  assert_eq!(w, Array::from(vec![6., 6., 6., 6., 6.]));

}

ベクトルノルム

ndarray-badge

このレシピでは、与えられたベクトルの l1 ノルムと l2 ノルムを計算する際の Array1 型、ArrayView1 型、 fold メソッド、および dot メソッドの使い方を示します。

  • l2_norm 関数は 2 つのうち単純な方であり、ベクトルとそれ自身との ドット積の平方根を計算します。
  • l1_norm 関数は、要素の絶対値を合計する fold 演算によって計算されます。(これは x.mapv(f64::abs).scalar_sum() でも実行できますが、その場合 mapv の結果を保持するための新しい 配列が確保されます。)

l1_norml2_norm はどちらも ArrayView1 型を受け取ることに注意してください。このレシピでは ベクトルノルムを扱うため、ノルム関数は一次元の ビューだけを受け取れれば十分です(したがって ArrayView1)。 これらの関数は代わりに &Array1<f64> 型の パラメータを取ることもできますが、その場合、呼び出し側は所有された配列への 参照を持っている必要があり、単にビューへアクセスできる場合よりも制約が強くなります (ビューは所有された配列だけでなく、任意の配列またはビューから作成できるためです)。

ArrayArrayView はどちらも ArrayBase の型エイリアスです。したがって、呼び出し側にとって 最も汎用的な引数型は &ArrayBase<S, Ix1> where S: Data になります。これは、呼び出し側が x.view() の代わりに &array または &view を 使えるようになるためです。 関数が公開 API の一部である場合、ユーザーの利便性のためにそれがより良い選択である 可能性があります。内部関数では、より簡潔な ArrayView1<f64> の方が望ましいかもしれません。


use ndarray::{array, Array1, ArrayView1};

fn l1_norm(x: ArrayView1<f64>) -> f64 {
    x.fold(0., |acc, elem| acc + elem.abs())
}

fn l2_norm(x: ArrayView1<f64>) -> f64 {
    x.dot(&x).sqrt()
}

fn normalize(mut x: Array1<f64>) -> Array1<f64> {
    let norm = l2_norm(x.view());
    x.mapv_inplace(|e| e/norm);
    x
}

fn main() {
    let x = array![1., 2., 3., 4., 5.];
    println!("||x||_2 = {}", l2_norm(x.view()));
    println!("||x||_1 = {}", l1_norm(x.view()));
    println!("Normalizing x yields {:?}", normalize(x));
}

行列を反転

nalgebra-badge cat-science-badge

nalgebra::Matrix3 で 3x3 行列を作成し、可能であればそれを反転します。


use nalgebra::Matrix3;

fn main() {
    let m1 = Matrix3::new(2.0, 1.0, 1.0, 3.0, 2.0, 1.0, 2.0, 1.0, 2.0);
    println!("m1 = {}", m1);
    match m1.try_inverse() {
        Some(inv) => {
            println!("The inverse of m1 is: {}", inv);
        }
        None => {
            println!("m1 is not invertible!");
        }
    }
}

行列をシリアライズ/デシリアライズする

ndarray-badge cat-science-badge

行列を JSON との間でシリアライズおよびデシリアライズします。シリアライズは serde_json::to_string が行い、serde_json::from_str がデシリアライズを行います。

シリアライズの後にデシリアライズすると、元の行列がそのまま返されることに注意してください。


use nalgebra::DMatrix;

fn main() -> Result<(), std::io::Error> {
    let row_slice: Vec<i32> = (1..5001).collect();
    let matrix = DMatrix::from_row_slice(50, 100, &row_slice);

    // 行列をシリアライズする
    let serialized_matrix = serde_json::to_string(&matrix)?;

    // 行列をデシリアライズする
    let deserialized_matrix: DMatrix<i32> = serde_json::from_str(&serialized_matrix)?;

    // `deserialized_matrix` が `matrix` と等しいことを検証する
    assert!(deserialized_matrix == matrix);

    Ok(())
}

三角法

三角形の辺の長さを計算する

std-badge cat-science-badge

角度が1ラジアンで、対辺の長さが80の直角三角形の斜辺の長さを計算します。

fn main() {
    let angle: f64 = 1.0;
    let side_length = 80.0;

    let hypotenuse = side_length / angle.sin();

    println!("Hypotenuse: {}", hypotenuse);
}

tan が sin を cos で割った値に等しいことの検証

std-badge cat-science-badge

x = 6 のとき、tan(x) が sin(x)/cos(x) に等しいことを検証します。

fn main() {
    let x: f64 = 6.0;

    let a = x.tan();
    let b = x.sin() / x.cos();

    assert_eq!(a, b);
}

地球上の2点間の距離

std-badge

デフォルトでは、Rust は三角関数、平方根、ラジアンと度の間の変換関数 などの数学的な float methods を提供しています。

次の例では、Haversine formula を使って、地球上の2点間の距離を キロメートル単位で計算します。点は、度単位の緯度と経度の組として 表されます。次に、to_radians でそれらをラジアンに変換します。 sincospowisqrt で中心角を計算し、 最後に、距離を計算できます。

fn main() {
    let earth_radius_kilometer = 6371.0_f64;
    let (paris_latitude_degrees, paris_longitude_degrees) = (48.85341_f64, -2.34880_f64);
    let (london_latitude_degrees, london_longitude_degrees) = (51.50853_f64, -0.12574_f64);

    let paris_latitude = paris_latitude_degrees.to_radians();
    let london_latitude = london_latitude_degrees.to_radians();

    let delta_latitude = (paris_latitude_degrees - london_latitude_degrees).to_radians();
    let delta_longitude = (paris_longitude_degrees - london_longitude_degrees).to_radians();

    let central_angle_inner = (delta_latitude / 2.0).sin().powi(2)
        + paris_latitude.cos() * london_latitude.cos() * (delta_longitude / 2.0).sin().powi(2);
    let central_angle = 2.0 * central_angle_inner.sqrt().asin();

    let distance = earth_radius_kilometer * central_angle;

    println!(
        "Distance between Paris and London on the surface of Earth is {:.1} kilometers",
        distance
    );
}

複素数

複素数を作成する

num-badge cat-science-badge

num::complex::Complex 型の複素数を作成します。複素数の実部と 虚部は、どちらも同じ型でなければなりません。


fn main() {
    let complex_integer = num::complex::Complex::new(10, 20);
    let complex_float = num::complex::Complex::new(10.1, 20.1);

    println!("Complex integer: {}", complex_integer);
    println!("Complex float: {}", complex_float);
}

複素数の加算

num-badge cat-science-badge

複素数に対して数学演算を行う方法は、組み込み型に対する場合と同じで、 対象の数値は同じ型(つまり浮動小数点数 または整数)でなければなりません。


fn main() {
    let complex_num1 = num::complex::Complex::new(10.0, 20.0); // 浮動小数点数を使用する必要があります
    let complex_num2 = num::complex::Complex::new(3.1, -4.2);

    let sum = complex_num1 + complex_num2;

    println!("Sum: {}", sum);
}

数学関数

num-badge cat-science-badge

複素数には、他の数学関数とどのように相互作用するかという点で さまざまな興味深い性質があり、特に正弦関数群 や数 e に関するものが挙げられます。複素数でこれらの関数を使うために、 Complex 型にはいくつかの組み込み関数があり、そのすべてはここにあります: num::complex::Complex.


use std::f64::consts::PI;
use num::complex::Complex;

fn main() {
    let x = Complex::new(0.0, 2.0*PI);

    println!("e^(2i * pi) = {}", x.exp()); // =~1
}

統計

中心傾向の指標

std-badge cat-science-badge

これらの例では、Rust 配列に格納されたデータセットの中心傾向の指標を計算します。データ集合が空の場合は平均値、中央値、最頻値を計算できないことがあるため、各関数は呼び出し側で処理する Option を返します。

最初の例では、データに対する参照のイテレータを生成し、sumlen を使ってそれぞれ合計値と値の個数を求めることで、平均値(すべての測定値の合計を集合内の測定値の数で割ったもの)を計算します。

fn main() {
    let data = [3, 1, 6, 1, 5, 8, 1, 8, 10, 11];

    let sum = data.iter().sum::<i32>() as f32;
    let count = data.len();

    let mean = match count {
       positive if positive > 0 => Some(sum  / count as f32),
       _ => None
    };

    println!("Mean of the data is {:?}", mean);
}

2 番目の例では、quickselect アルゴリズムを使って中央値を計算します。このアルゴリズムでは、中央値を含む可能性があると分かっているデータセットの部分だけをソートすることで、全体の sort を避けます。これは cmpOrdering を使って次に調べるパーティションを簡潔に決定し、split_at を使って各ステップで次のパーティションの任意のピボットを選択します。

use std::cmp::Ordering;

fn partition(data: &[i32]) -> Option<(Vec<i32>, i32, Vec<i32>)> {
    match data.len() {
        0 => None,
        _ => {
            let (pivot_slice, tail) = data.split_at(1);
            let pivot = pivot_slice[0];
            let (left, right) = tail.iter()
                .fold((vec![], vec![]), |mut splits, next| {
                    {
                        let (ref mut left, ref mut right) = &mut splits;
                        if next < &pivot {
                            left.push(*next);
                        } else {
                            right.push(*next);
                        }
                    }
                    splits
                });

            Some((left, pivot, right))
        }
    }
}

fn select(data: &[i32], k: usize) -> Option<i32> {
    let part = partition(data);

    match part {
        None => None,
        Some((left, pivot, right)) => {
            let pivot_idx = left.len();

            match pivot_idx.cmp(&k) {
                Ordering::Equal => Some(pivot),
                Ordering::Greater => select(&left, k),
                Ordering::Less => select(&right, k - (pivot_idx + 1)),
            }
        },
    }
}

fn median(data: &[i32]) -> Option<f32> {
    let size = data.len();

    match size {
        even if even % 2 == 0 => {
            let fst_med = select(data, (even / 2) - 1);
            let snd_med = select(data, even / 2);

            match (fst_med, snd_med) {
                (Some(fst), Some(snd)) => Some((fst + snd) as f32 / 2.0),
                _ => None
            }
        },
        odd => select(data, odd / 2).map(|x| x as f32)
    }
}

fn main() {
    let data = [3, 1, 6, 1, 5, 8, 1, 8, 10, 11];

    let part = partition(&data);
    println!("Partition is {:?}", part);

    let sel = select(&data, 5);
    println!("Selection at ordered index {} is {:?}", 5, sel);

    let med = median(&data);
    println!("Median is {:?}", med);
}

最後の例では、可変の HashMap を使って集合内の各整数値の出現回数を集計し、foldentry API を利用して最頻値を計算します。HashMap 内で最も頻繁に現れる値は max_by_key で求められます。

use std::collections::HashMap;

fn main() {
    let data = [3, 1, 6, 1, 5, 8, 1, 8, 10, 11];

    let frequencies = data.iter().fold(HashMap::new(), |mut freqs, value| {
        *freqs.entry(value).or_insert(0) += 1;
        freqs
    });

    let mode = frequencies
        .into_iter()
        .max_by_key(|&(_, count)| count)
        .map(|(value, _)| *value);

    println!("Mode of the data is {:?}", mode);
}

標準偏差

std-badge cat-science-badge

この例では、一連の測定値の標準偏差と z スコアを計算します。

標準偏差は分散の平方根として定義されます(ここでは f32 の [sqrt] で計算します)。分散は、各測定値と [mean] の差を二乗したものの sum を測定値の個数で割ったものです。

z スコアは、1 つの測定値がデータセットの [mean] から何個分の標準偏差だけ離れているかを表す値です。

fn mean(data: &[i32]) -> Option<f32> {
    let sum = data.iter().sum::<i32>() as f32;
    let count = data.len();

    match count {
        positive if positive > 0 => Some(sum / count as f32),
        _ => None,
    }
}

fn std_deviation(data: &[i32]) -> Option<f32> {
    match (mean(data), data.len()) {
        (Some(data_mean), count) if count > 0 => {
            let variance = data.iter().map(|value| {
                let diff = data_mean - (*value as f32);

                diff * diff
            }).sum::<f32>() / count as f32;

            Some(variance.sqrt())
        },
        _ => None
    }
}

fn main() {
    let data = [3, 1, 6, 1, 5, 8, 1, 8, 10, 11];

    let data_mean = mean(&data);
    println!("Mean is {:?}", data_mean);

    let data_std_deviation = std_deviation(&data);
    println!("Standard deviation is {:?}", data_std_deviation);

    let zscore = match (data_mean, data_std_deviation) {
        (Some(mean), Some(std_deviation)) => {
            let diff = data[4] as f32 - mean;

            Some(diff / std_deviation)
        },
        _ => None
    };
    println!("Z-score of data at index 4 (with value {}) is {:?}", data[4], zscore);
}

その他

多倍長整数

num-badge cat-science-badge

BigInt を使うと、128 ビットを超える整数の計算が可能です。


use num::bigint::{BigInt, ToBigInt};

fn factorial(x: i32) -> BigInt {
    if let Some(mut factorial) = 1.to_bigint() {
        for i in 1..=x {
            factorial = factorial * i;
        }
        factorial
    }
    else {
        panic!("Failed to calculate factorial!");
    }
}

fn main() {
    println!("{}! equals {}", 100, factorial(100));
}

セーフティクリティカルな Rust

パニックしないことの保証

コンパイル時の No-Panic 保証

no-panic-badge cat-no-std-badge cat-rust-patterns-badge

安全性が重要なシステム—自動車のブレーキ、医療機器、航空宇宙—では、 panic! は単なるクラッシュではなく、致命的な障害です。array[i] のような 標準的な操作では、境界外アクセス時にパニックする隠れた境界チェックが挿入されます。

#[no_panic] 属性マクロは、関数がパニックするコードパスに決して到達しないことを リンク時にコンパイラに証明させます。証明できなければ、ビルドは失敗します。

パニックするパターン安全な代替手段
slice[i]slice.get(i) を網羅的な match とともに使用
for i in 0..len { slice[i] }for &v in slice(イテレータ)
value.unwrap()match / if let / ?
a / b(整数、b が 0 の可能性がある)除算前に b をチェックする

以下の例は、3 つのパニックしない関数—集計、検索、センサーの正規化—を示しており、 それぞれが #[no_panic] によってコンパイル時に証明されます。

use no_panic::no_panic;

/// Sums a slice without any operation that could panic.
///
/// In safety-critical code, a `panic!` is a catastrophic failure.
/// Standard indexing like `slice[i]` inserts a bounds check that
/// calls `panic!` on out-of-bounds access.  Iterators avoid this
/// entirely—the compiler can prove no panic path exists.
#[no_panic]
fn safe_sum(values: &[i32]) -> i64 {
    let mut total: i64 = 0;
    for &v in values {
        total += v as i64;
    }
    total
}

/// Looks up a value by index, returning `None` instead of panicking.
///
/// Using `get()` + exhaustive pattern matching guarantees every code
/// path is handled.  The `#[no_panic]` attribute makes the compiler
/// **prove** it at link time—if any hidden panic path remains, the
/// build fails.
#[no_panic]
fn safe_lookup(data: &[u8], index: usize) -> Option<u8> {
    match data.get(index) {
        Some(&val) => Some(val),
        None => None,
    }
}

/// Clamps a sensor reading into a valid range without panicking.
///
/// Real-world example: an ADC returns a raw `u16` that must be
/// mapped to 0–100 %.  Using `clamp` and simple arithmetic keeps
/// the function panic-free.
#[no_panic]
fn normalize_sensor(raw: u16, min: u16, max: u16) -> f32 {
    if max == min {
        return 0.0;
    }
    let clamped = raw.clamp(min, max);
    (clamped - min) as f32 / (max - min) as f32
}

fn main() {
    let readings = [10, 20, 30, 40, 50];

    let total = safe_sum(&readings);
    println!("sum = {total}");
    assert_eq!(total, 150);

    let val = safe_lookup(&[0xAA, 0xBB, 0xCC], 1);
    println!("lookup index 1 = {val:?}");
    assert_eq!(val, Some(0xBB));

    let miss = safe_lookup(&[0xAA, 0xBB, 0xCC], 99);
    println!("lookup index 99 = {miss:?}");
    assert_eq!(miss, None);

    let pct = normalize_sensor(2048, 0, 4095);
    println!("sensor = {pct:.2}%");
    assert!((pct - 0.5).abs() < 0.01);
}

決定論的メモリ

Heapless コレクションによる決定論的メモリ

heapless-badge cat-no-std-badge cat-data-structures-badge

安全性が重要なシステムやリアルタイムシステムでは、ヒープの使用を完全に禁止していることが少なくありません。グローバルアロケータは非決定的であり、割り当て時間は一定ではなく、長時間稼働するファームウェアでは断片化や、気づかれない OOM 障害のリスクがあります。

heapless は、データをコンパイル時に容量が固定された スタック(または static)上に保持する VecStringDeque、およびマップ型を提供します。 容量を超えて追加しようとすると、パニックや割り当てを行う代わりに Err が返されるため、呼び出し側がそれをどう処理するかを決定できます。

std の型heapless の対応型保証
Vec<T>heapless::Vec<T, N>要素数は最大 N、アロケータ不要
Stringheapless::String<N>最大 N バイト、アロケータ不要
VecDeque<T>heapless::Deque<T, N>固定容量のリングバッファ
HashMap<K,V>heapless::IndexMap<K,V,S,N>固定容量のハッシュマップ

以下の例では、スタックに割り当てられたイベントログを構築し、 容量制限の適用を示しています。これは、組み込みシステムやリアルタイムシステムで 求められる、予測可能で一定メモリの動作の一例です。

use heapless::{String, Vec};
use thiserror::Error;

#[derive(Debug, Error)]
enum LogError {
    #[error("log full (capacity exceeded)")]
    Full,
    #[error("log is empty")]
    Empty,
    #[error(transparent)]
    Fmt(#[from] core::fmt::Error),
}

/// A fixed-capacity event log that never touches the heap.
///
/// In real-time and safety-critical systems the global allocator is
/// forbidden because allocation time is unbounded and fragmentation
/// can cause silent OOM failures in long-running firmware.
///
/// [`heapless::Vec`] stores up to `N` elements on the stack (or in a
/// `static`).  The capacity is fixed at compile time, so the memory
/// footprint is constant and predictable.
struct EventLog<const N: usize> {
    entries: Vec<Event, N>,
}

#[derive(Debug, Clone)]
struct Event {
    timestamp_ms: u32,
    code: u16,
}

impl<const N: usize> EventLog<N> {
    /// Creates an empty log.
    fn new() -> Self {
        Self {
            entries: Vec::new(),
        }
    }

    /// Records an event.  Returns `Err` if the log is full instead
    /// of panicking or allocating—the caller decides what to do.
    fn record(&mut self, timestamp_ms: u32, code: u16) -> Result<(), LogError> {
        let event = Event { timestamp_ms, code };
        self.entries.push(event).map_err(|_| LogError::Full)
    }

    /// Returns how many events have been recorded.
    fn len(&self) -> usize {
        self.entries.len()
    }

    /// Returns the most recent event, if any.
    fn latest(&self) -> Result<&Event, LogError> {
        self.entries.last().ok_or(LogError::Empty)
    }
}

/// Formats a sensor label without heap allocation.
///
/// [`heapless::String<N>`] works like `std::string::String` but
/// stores up to `N` bytes on the stack.  `write!` returns `Err` if
/// the formatted text would exceed capacity.
fn format_label(sensor_id: u16, value: f32) -> Result<String<32>, LogError> {
    use core::fmt::Write;
    let mut buf: String<32> = String::new();
    write!(buf, "S{sensor_id}={value:.1}")?;
    Ok(buf)
}

fn main() -> Result<(), LogError> {
    // A log that holds at most 8 events — zero heap allocation.
    let mut log: EventLog<8> = EventLog::new();

    log.record(100, 0x01)?;
    log.record(200, 0x02)?;
    log.record(300, 0xFF)?;

    println!("logged {} events", log.len());
    let latest = log.latest()?;
    println!(
        "latest: timestamp={}ms code=0x{:02X}",
        latest.timestamp_ms, latest.code
    );

    // Stack-allocated string formatting.
    let label = format_label(42, 3.14)?;
    println!("label: {label}");

    // Demonstrate capacity enforcement — the 9th push returns Err.
    let mut full_log: EventLog<2> = EventLog::new();
    full_log.record(0, 1)?;
    full_log.record(1, 2)?;
    assert!(full_log.record(2, 3).is_err());
    println!("overflow correctly rejected");

    Ok(())
}

テキスト処理

正規表現

メールアドレスからログイン名を検証して抽出する

regex-badge lazy_static-badge cat-text-processing-badge

メールアドレスが正しい形式であることを検証し、@ 記号より前のすべてを抽出します。

use lazy_static::lazy_static;
use regex::Regex;

fn extract_login(input: &str) -> Option<&str> {
    lazy_static! {
        static ref RE: Regex = Regex::new(r"(?x)
            ^(?P<login>[^@\s]+)@
            ([[:word:]]+\.)*
            [[:word:]]+$
            ").unwrap();
    }
    RE.captures(input).and_then(|cap| {
        cap.name("login").map(|login| login.as_str())
    })
}

fn main() {
    assert_eq!(extract_login(r"I❤email@example.com"), Some(r"I❤email"));
    assert_eq!(
        extract_login(r"sdf+sdsfsd.as.sdsd@jhkk.d.rl"),
        Some(r"sdf+sdsfsd.as.sdsd")
    );
    assert_eq!(extract_login(r"More@Than@One@at.com"), None);
    assert_eq!(extract_login(r"Not an email@email"), None);
}

テキストから一意な #Hashtags のリストを抽出する

regex-badge lazy_static-badge cat-text-processing-badge

テキストからハッシュタグのリストを抽出し、ソートして重複を排除します。

ここで示すハッシュタグ用の regex は、文字で始まるラテン文字のハッシュタグだけを検出します。完全な twitter hashtag regex は、はるかに複雑です。

use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashSet;

fn extract_hashtags(text: &str) -> HashSet<&str> {
    lazy_static! {
        static ref HASHTAG_REGEX : Regex = Regex::new(
                r"\#[a-zA-Z][0-9a-zA-Z_]*"
            ).unwrap();
    }
    HASHTAG_REGEX.find_iter(text).map(|mat| mat.as_str()).collect()
}

fn main() {
    let tweet = "Hey #world, I just got my new #dog, say hello to Till. #dog #forever #2 #_ ";
    let tags = extract_hashtags(tweet);
    assert!(tags.contains("#dog") && tags.contains("#forever") && tags.contains("#world"));
    assert_eq!(tags.len(), 3);
}

テキストから電話番号を抽出する

regex-badge cat-text-processing-badge

Regex::captures_iter を使用してテキスト文字列を処理し、複数の電話番号をキャプチャします。ここでの例は、米国式の電話番号を対象としています。

use anyhow::Result;
use regex::Regex;
use std::fmt;

struct PhoneNumber<'a> {
    area: &'a str,
    exchange: &'a str,
    subscriber: &'a str,
}

impl<'a> fmt::Display for PhoneNumber<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "1 ({}) {}-{}", self.area, self.exchange, self.subscriber)
    }
}

fn main() -> Result<()> {
    let phone_text = "
    +1 505 881 9292 (v) +1 505 778 2212 (c) +1 505 881 9297 (f)
    (202) 991 9534
    Alex 5553920011
    1 (800) 233-2010
    1.299.339.1020";

    let re = Regex::new(
        r#"(?x)
          (?:\+?1)?                       # 国番号(任意)
          [\s\.]?
          (([2-9]\d{2})|\(([2-9]\d{2})\)) # 市外局番
          [\s\.\-]?
          ([2-9]\d{2})                    # 交換局コード
          [\s\.\-]?
          (\d{4})                         # 加入者番号"#,
    )?;

    let phone_numbers = re.captures_iter(phone_text).filter_map(|cap| {
        let groups = (cap.get(2).or(cap.get(3)), cap.get(4), cap.get(5));
        match groups {
            (Some(area), Some(ext), Some(sub)) => Some(PhoneNumber {
                area: area.as_str(),
                exchange: ext.as_str(),
                subscriber: sub.as_str(),
            }),
            _ => None,
        }
    });

    assert_eq!(
        phone_numbers.map(|m| m.to_string()).collect::<Vec<_>>(),
        vec![
            "1 (505) 881-9292",
            "1 (505) 778-2212",
            "1 (505) 881-9297",
            "1 (202) 991-9534",
            "1 (555) 392-0011",
            "1 (800) 233-2010",
            "1 (299) 339-1020",
        ]
    );

    Ok(())
}

複数の正規表現に一致する行でログファイルをフィルタリングする

regex-badge cat-text-processing-badge

application.log という名前のファイルを読み込み、“version X.X.X”、 ポート 443 が続く何らかの IP アドレス (例: “192.168.0.1:443”)、または特定の警告を含む行だけを出力します。

regex::RegexSetBuilderregex::RegexSet を構成します。 正規表現ではバックスラッシュが非常によく使われるため、raw string literals を使うと読みやすくなります。

use anyhow::Result;
use std::fs::File;
use std::io::{BufReader, BufRead};
use regex::RegexSetBuilder;

fn main() -> Result<()> {
    let log_path = "application.log";
    let buffered = BufReader::new(File::open(log_path)?);

    let set = RegexSetBuilder::new(&[
        r#"version "\d\.\d\.\d""#,
        r#"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:443"#,
        r#"warning.*timeout expired"#,
    ]).case_insensitive(true)
        .build()?;

    buffered
        .lines()
        .filter_map(|line| line.ok())
        .filter(|line| set.is_match(line.as_str()))
        .for_each(|x| println!("{}", x));

    Ok(())
}

あるテキストパターンのすべての出現箇所を別のパターンに置き換えます。

regex-badge lazy_static-badge cat-text-processing-badge

標準の ISO 8601 の日付パターン YYYY-MM-DD に一致するすべての箇所を、 スラッシュ区切りのアメリカ英語の日付表現に置き換えます。 たとえば 2013-01-1501/15/2013 になります。

メソッド Regex::replace_all は、正規表現全体に一致するすべての箇所を置き換えます。 &strReplacer トレイトを実装しているため、$abcde のような変数で、 検索用正規表現内の対応する名前付きキャプチャグループ (?P<abcde>REGEX) を参照できます。 例とエスケープの詳細については、replacement string syntax を参照してください。

use lazy_static::lazy_static;

use std::borrow::Cow;
use regex::Regex;

fn reformat_dates(before: &str) -> Cow<str> {
    lazy_static! {
        static ref ISO8601_DATE_REGEX : Regex = Regex::new(
            r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})"
            ).unwrap();
    }
    ISO8601_DATE_REGEX.replace_all(before, "$m/$d/$y")
}

fn main() {
    let before = "2012-03-14, 2013-01-15 and 2014-07-05";
    let after = reformat_dates(before);
    assert_eq!(after, "03/14/2012, 01/15/2013 and 07/05/2014");
}

文字列のパース

Unicode 書記素を収集する

unicode-segmentation-badge cat-text-processing-badge

unicode-segmentation クレートの UnicodeSegmentation::graphemes 関数を使用して、UTF-8 文字列から個々の Unicode 書記素を収集します。

use unicode_segmentation::UnicodeSegmentation;

fn main() {
    let name = "José Guimarães\r\n";
    let graphemes = UnicodeSegmentation::graphemes(name, true)
    	.collect::<Vec<&str>>();
	assert_eq!(graphemes[3], "é");
}

カスタム structFromStr トレイトを実装する

std-badge cat-text-processing-badge

カスタム構造体 RGB を作成し、FromStr トレイトを実装して、指定されたカラーの16進コードを RGB カラーコードに変換します。

use std::str::FromStr;

#[derive(Debug, PartialEq)]
struct RGB {
    r: u8,
    g: u8,
    b: u8,
}

impl FromStr for RGB {
    type Err = std::num::ParseIntError;

    // '#rRgGbB..' 形式のカラー16進コードを
    // 'RGB' のインスタンスにパースする
    fn from_str(hex_code: &str) -> Result<Self, Self::Err> {
	
        // u8::from_str_radix(src: &str, radix: u32) は、指定された基数の文字列
        // スライスを u8 に変換する
        let r: u8 = u8::from_str_radix(&hex_code[1..3], 16)?;
        let g: u8 = u8::from_str_radix(&hex_code[3..5], 16)?;
        let b: u8 = u8::from_str_radix(&hex_code[5..7], 16)?;

        Ok(RGB { r, g, b })
    }
}

fn main() {
    let code: &str = &r"#fa7268";
    match RGB::from_str(code) {
        Ok(rgb) => {
            println!(
                r"RGB カラーコード: R: {} G: {} B: {}",
                rgb.r, rgb.g, rgb.b
            );
        }
        Err(_) => {
            println!("{} は有効なカラー16進コードではありません!", code);
        }
    }

    // from_str が期待どおりに動作するかをテストする
    assert_eq!(
        RGB::from_str(&r"#fa7268").unwrap(),
        RGB {
            r: 250,
            g: 114,
            b: 104
        }
    );
}

Webプログラミング

Webページのスクレイピング

Uniform Resource Location(URL)

メディアタイプ(MIME)

クライアント

Web認証

レシピクレートカテゴリ
Basic認証reqwest-badgecat-net-badge

フルスタックWeb

リンクの抽出

Web ページの HTML からすべてのリンクを抽出する

reqwest-badge select-badge cat-net-badge

reqwest::get を使用して HTTP GET リクエストを実行し、次に Document::from_read を使用してレスポンスを HTML ドキュメントとして解析します。 Name の条件に “a” を指定した find は、すべてのリンクを取得します。 Selection に対して filter_map を呼び出すと、URL を取得できます “href” attr(属性)を持つリンクから。

// select には rand v.0.8 が必要です
// cargo-deps: tokio="1", reqwest="0.11", select="0.6", thiserror="1"
mod links {
  use thiserror::Error;
  use select::document::Document;
  use select::predicate::Name;

  #[derive(Error, Debug)]
  pub enum LinkError {
      #[error("Reqwest エラー: {0}")]
      ReqError(#[from] reqwest::Error),
      #[error("IO エラー: {0}")]
      IoError(#[from] std::io::Error),
  }

  pub async fn get_links(page: &str) -> Result<Vec<Box<str>>, LinkError> {
    let res = reqwest::get(page)
      .await?
      .text()
      .await?;

    let links = Document::from(res.as_str())
      .find(Name("a"))
      .filter_map(|node| node.attr("href"))
      .into_iter()
      .map(|link| Box::<str>::from(link.to_string()))
      .collect();

    Ok(links)
  }
}

#[tokio::main]
async fn main() -> Result<(), links::LinkError> {
    let page_links = links::get_links("https://www.rust-lang.org/en-US/").await?;
    for link in page_links {
        println!("{}", link);
    }
    Ok(())
}

Web ページのリンク切れを確認する

reqwest-badge select-badge url-badge cat-net-badge

get_base_url を呼び出してベース URL を取得します。ドキュメントに base タグがある場合は、 base タグから href attr を取得します。元の URL の Position::BeforePath がデフォルトとして機能します。

ドキュメント内のリンクを反復処理し、各リンクを url::ParseOptionsUrl::parse でパースする tokio::task::spawn タスクを作成します。 そのタスクは reqwest でリンクにリクエストを送信し、 StatusCode を検証します。 その後、プログラムを終了する前に 各タスクの完了を await します。

// cargo-deps: tokio="1", reqwest="0.11", select="0.6", thiserror="1", url="2", anyhow="1"
mod broken {
  use thiserror::Error;
  use reqwest::StatusCode;
  use select::document::Document;
  use select::predicate::Name;
  use std::collections::HashSet;
  use url::{Position, Url};

  #[derive(Error, Debug)]
  pub enum BrokenError {
      #[error("Reqwest error: {0}")]
      ReqError(#[from] reqwest::Error),
      #[error("IO error: {0}")]
      IoError(#[from] std::io::Error),
      #[error("URL parse error: {0}")]
      UrlParseError(#[from] url::ParseError),
      #[error("Join error: {0}")]
      JoinError(#[from] tokio::task::JoinError),
  }

  pub struct CategorizedUrls {
      pub ok: Vec<String>,
      pub broken: Vec<String>,
  }

  enum Link {
      GoodLink(Url),
      BadLink(Url),
  }

  async fn get_base_url(url: &Url, doc: &Document) -> Result<Url, BrokenError> {
    let base_tag_href = doc.find(Name("base")).filter_map(|n| n.attr("href")).nth(0);
    let base_url =
      base_tag_href.map_or_else(|| Url::parse(&url[..Position::BeforePath]), Url::parse)?;
    Ok(base_url)
  }

  async fn check_link(url: &Url) -> Result<bool, BrokenError> {
    let res = reqwest::get(url.as_ref()).await?;
    Ok(res.status() != StatusCode::NOT_FOUND)
  }

  pub async fn check(site: &str) -> Result<CategorizedUrls, BrokenError> {
    let url = Url::parse(site)?;
    let res = reqwest::get(url.as_ref()).await?.text().await?;
    let document = Document::from(res.as_str());
    let base_url = get_base_url(&url, &document).await?;
    let base_parser = Url::options().base_url(Some(&base_url));
    let links: HashSet<Url> = document
      .find(Name("a"))
      .filter_map(|n| n.attr("href"))
      .filter_map(|link| base_parser.parse(link).ok())
      .collect();
      let mut tasks = vec![];
      let mut ok = vec![];
      let mut broken = vec![];

      for link in links {
          tasks.push(tokio::spawn(async move {
              if check_link(&link).await.unwrap_or(false) {
                  Link::GoodLink(link) 
              } else {
                  Link::BadLink(link)
              }
          }));
      }

      for task in tasks {
          match task.await? {
              Link::GoodLink(link) => ok.push(link.to_string()),
              Link::BadLink(link) => broken.push(link.to_string()),
          }
      }

    Ok(CategorizedUrls { ok, broken })
  }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let categorized = broken::check("https://www.rust-lang.org/en-US/").await?;
    println!("OK: {:?}", categorized.ok);
    println!("Broken: {:?}", categorized.broken);
    Ok(())
}

MediaWikiマークアップから一意なリンクをすべて抽出する

reqwest-badge regex-badge cat-net-badge

reqwest::get を使って MediaWiki ページのソースを取得し、その後 Regex::captures_iter を使って内部リンクと外部リンクのすべての出現箇所を 検索します。Cow を使用すると、String の過剰な割り当てを避けられます。

MediaWiki のリンク構文はこちらで説明されています。 呼び出し元の 関数は文書全体を保持し、リンクは元の文書を参照するスライス として返されます。

// cargo-deps: tokio="1", reqwest="0.11", regex="1", anyhow="1"
mod wiki {
  use regex::Regex;
  use std::borrow::Cow;
  use std::collections::HashSet;
  use std::sync::LazyLock;

  pub fn extract_links(content: &str) -> HashSet<Cow<str>> {
    static WIKI_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(
        r"(?x)
                  \[\[(?P<internal>[^\[\]|]*)[^\[\]]*\]\]    # 内部リンク
                  |
                  (url=|URL\||\[)(?P<external>http.*?)[ \|}] # 外部リンク
              "
      )
      .unwrap()
    );

    let links: HashSet<_> = WIKI_REGEX
      .captures_iter(content)
      .map(|c| match (c.name("internal"), c.name("external")) {
          (Some(val), None) => Cow::from(val.as_str()),
          (None, Some(val)) => Cow::from(val.as_str()),
          _ => unreachable!(),
      })
      .collect::<HashSet<_>>();

    links
  }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let content = reqwest::get(
    "https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&action=raw",
  )
  .await?
  .text()
  .await?;

  println!("{:#?}", wiki::extract_links(content.as_str()));

  Ok(())
}

一様リソースロケーション

文字列から URL を Url 型に解析する

url-badge cat-net-badge

url クレートの parse メソッドは、&str を検証して Url 構造体に解析します。入力文字列は不正な形式である可能性があるため、このメソッドは Result<Url, ParseError> を返します。

URL が解析されると、Url 型のすべてのメソッドで使用できます。

use url::{Url, ParseError};

fn main() -> Result<(), ParseError> {
    let s = "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open";

    let parsed = Url::parse(s)?;
    println!("The path part of the URL is: {}", parsed.path());

    Ok(())
}

パスセグメントを削除してベース URL を作成する

url-badge cat-net-badge

ベース URL にはプロトコルとドメインが含まれます。ベース URL にはフォルダー、 ファイル、クエリ文字列はありません。これらの各項目は、指定された URL から取り除かれます。PathSegmentsMut::clear はパスを削除し、Url::set_query は クエリ文字列を削除します。

use anyhow::{Result, anyhow};
use url::Url;

fn main() -> Result<()> {
    let full = "https://github.com/rust-lang/cargo?asdf";

    let url = Url::parse(full)?;
    let base = base_url(url)?;

    assert_eq!(base.as_str(), "https://github.com/");
    println!("The base of the URL is: {}", base);

    Ok(())
}

fn base_url(mut url: Url) -> Result<Url> {
    match url.path_segments_mut() {
        Ok(mut path) => {
            path.clear();
        }
        Err(_) => {
            return Err(anyhow!("Cannot be a base URL"));
        }
    }

    url.set_query(None);

    Ok(url)
}

ベース URL から新しい URL を作成する

url-badge cat-net-badge

join メソッドは、ベース URL と相対パスから新しい URL を作成します。

use url::{Url, ParseError};

fn main() -> Result<(), ParseError> {
    let path = "/rust-lang/cargo";

    let gh = build_github_url(path)?;

    assert_eq!(gh.as_str(), "https://github.com/rust-lang/cargo");
    println!("The joined URL is: {}", gh);

    Ok(())
}

fn build_github_url(path: &str) -> Result<Url, ParseError> {
    const GITHUB: &'static str = "https://github.com";

    let base = Url::parse(GITHUB).expect("hardcoded URL is known to be valid");
    let joined = base.join(path)?;

    Ok(joined)
}

URL のオリジンを抽出する(スキーム / ホスト / ポート)

url-badge cat-net-badge

Url 構造体は、それが表す URL から情報を抽出するためのさまざまなメソッドを公開しています。

use url::{Url, Host, ParseError};

fn main() -> Result<(), ParseError> {
    let s = "ftp://rust-lang.org/examples";

    let url = Url::parse(s)?;

    assert_eq!(url.scheme(), "ftp");
    assert_eq!(url.host(), Some(Host::Domain("rust-lang.org")));
    assert_eq!(url.port_or_known_default(), Some(21));
    println!("The origin is as expected!");

    Ok(())
}

origin は同じ結果を生成します。

use anyhow::Result;
use url::{Url, Origin, Host};

fn main() -> Result<()> {
    let s = "ftp://rust-lang.org/examples";

    let url = Url::parse(s)?;

    let expected_scheme = "ftp".to_owned();
    let expected_host = Host::Domain("rust-lang.org".to_owned());
    let expected_port = 21;
    let expected = Origin::Tuple(expected_scheme, expected_host, expected_port);

    let origin = url.origin();
    assert_eq!(origin, expected);
    println!("The origin is as expected!");

    Ok(())
}

URL からフラグメント識別子とクエリペアを削除する

url-badge cat-net-badge

Url を解析し、url::Position でスライスして不要な URL の部分を取り除きます。

use url::{Url, Position, ParseError};

fn main() -> Result<(), ParseError> {
    let parsed = Url::parse("https://github.com/rust-lang/rust/issues?labels=E-easy&state=open")?;
    let cleaned: &str = &parsed[..Position::AfterPath];
    println!("cleaned: {}", cleaned);
    Ok(())
}

メディアタイプ

文字列から MIME タイプを取得する

mime-badge cat-encoding-badge

次の例では、mime クレートを使用して文字列から MIME タイプをパースする方法を示します。FromStrError は、unwrap_or 句でデフォルトの MIME タイプを生成します。

use mime::{Mime, APPLICATION_OCTET_STREAM};

fn main() {
    let invalid_mime_type = "i n v a l i d";
    let default_mime = invalid_mime_type
        .parse::<Mime>()
        .unwrap_or(APPLICATION_OCTET_STREAM);

    println!(
        "MIME for {:?} used default value {:?}",
        invalid_mime_type, default_mime
    );

    let valid_mime_type = "TEXT/PLAIN";
    let parsed_mime = valid_mime_type
        .parse::<Mime>()
        .unwrap_or(APPLICATION_OCTET_STREAM);

    println!(
        "MIME for {:?} was parsed as {:?}",
        valid_mime_type, parsed_mime
    );
}

ファイル名から MIME タイプを取得する

mime-badge cat-encoding-badge

次の例では、mime クレートを使用して、与えられた ファイル名から正しい MIME タイプを返す方法を示します。プログラムはファイル拡張子を 確認し、既知のリストと照合します。戻り値は mime:Mime です。

use mime::Mime;

fn find_mimetype (filename : &String) -> Mime{

    let parts : Vec<&str> = filename.split('.').collect();

    let res = match parts.last() {
            Some(v) =>
                match *v {
                    "png" => mime::IMAGE_PNG,
                    "jpg" => mime::IMAGE_JPEG,
                    "json" => mime::APPLICATION_JSON,
                    &_ => mime::TEXT_PLAIN,
                },
            None => mime::TEXT_PLAIN,
        };
    return res;
}

fn main() {
    let filenames = vec!("foobar.jpg", "foo.bar", "foobar.png");
    for file in filenames {
	    let mime = find_mimetype(&file.to_owned());
	 	println!("MIME for {}: {}", file, mime);
	 }

}

HTTP レスポンスの MIME タイプを解析する

reqwest-badge mime-badge cat-net-badge cat-encoding-badge

reqwest から HTTP レスポンスを受け取ると、MIME type またはメディアタイプは Content-Type ヘッダーに含まれていることがあります。reqwest::header::HeaderMap::get は このヘッダーを reqwest::header::HeaderValue として取得し、これは文字列に変換できます。 その後 mime クレートでそれを解析すると、mime::Mime 値が得られます。

mime クレートでは、よく使われる MIME タイプもいくつか定義されています。

なお、reqwest::header モジュールは http クレートからエクスポートされています。

use anyhow::Result;
use mime::Mime;
use std::str::FromStr;
use reqwest::header::CONTENT_TYPE;

#[tokio::main]
async fn main() -> Result<()> {
    let response = reqwest::get("https://www.rust-lang.org/logos/rust-logo-32x32.png").await?;
    let headers = response.headers();

    match headers.get(CONTENT_TYPE) {
        None => {
            println!("The response does not contain a Content-Type header.");
        }
        Some(content_type) => {
            let content_type = Mime::from_str(content_type.to_str()?)?;
            let media_type = match (content_type.type_(), content_type.subtype()) {
                (mime::TEXT, mime::HTML) => "a HTML document",
                (mime::TEXT, _) => "a text document",
                (mime::IMAGE, mime::PNG) => "a PNG image",
                (mime::IMAGE, _) => "an image",
                _ => "neither text nor image",
            };

            println!("The reponse contains {}.", media_type);
        }
    };

    Ok(())
}

クライアント

クライアント

リクエストを行う

HTTP GET リクエストを行う

reqwest-badge cat-net-badge

指定された URL を解析し、同期的な HTTP GET リクエストを reqwest::blocking::get で実行します。取得した reqwest::blocking::Response のステータスとヘッダーを表示します。HTTP レスポンスボディを、確保された Stringread_to_string を使って読み込みます。

use anyhow::Result;
use std::io::Read;

fn main() -> Result<()> {
    let mut res = reqwest::blocking::get("http://httpbin.org/get")?;
    let mut body = String::new();
    res.read_to_string(&mut body)?;

    println!("Status: {}", res.status());
    println!("Headers:\n{:#?}", res.headers());
    println!("Body:\n{}", body);

    Ok(())
}

非同期

同様のアプローチとして、tokio エグゼキューターを含めて main 関数を非同期にし、同じ情報を取得することもできます。

この例では、tokio::main がエグゼキューターの面倒なセットアップをすべて処理し、 .await までブロックせずに逐次的なコードを実装できるようにします。

reqwest の非同期版である reqwest::getreqwest::Response の両方を使用します。

use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let res = reqwest::get("http://httpbin.org/get").await?;
    println!("Status: {}", res.status());
    println!("Headers:\n{:#?}", res.headers());

    let body = res.text().await?;
    println!("Body:\n{}", body);
    Ok(())
}

REST リクエストのカスタムヘッダーと URL パラメーターを設定する

reqwest-badge serde-badge url-badge cat-net-badge

HTTP GET リクエストに対して、標準およびカスタムの HTTP ヘッダーと、 URL パラメーターの両方を設定します。

Url::parse_with_params を使って複雑な URL を構築します。標準 ヘッダー header::USER_AGENTheader::AUTHORIZATION、さらにカスタムの X-Powered-By ヘッダーを RequestBuilder::header を使って設定し、その後 RequestBuilder::send でリクエストを実行します。

リクエスト先の http://httpbin.org/headers は、 簡単に検証できるよう、すべてのリクエストヘッダーを含む JSON の辞書を返します。

use anyhow::Result;
use reqwest::Url;
use reqwest::blocking::Client;
use reqwest::header::{AUTHORIZATION, USER_AGENT};
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Deserialize, Debug)]
pub struct HeadersEcho {
    pub headers: HashMap<String, String>,
}

fn main() -> Result<()> {
    let url = Url::parse_with_params(
        "http://httpbin.org/headers",
        &[("lang", "rust"), ("browser", "servo")],
    )?;

    let response = Client::new()
        .get(url)
        .header(USER_AGENT, "Rust-test-agent")
        .header(AUTHORIZATION, "Bearer my-token")
        .header("X-Powered-By", "Rust")
        .send()?;

    assert_eq!(
        response.url().as_str(),
        "http://httpbin.org/headers?lang=rust&browser=servo"
    );

    let out: HeadersEcho = response.json()?;
    assert_eq!(out.headers["User-Agent"], "Rust-test-agent");
    assert_eq!(out.headers["Authorization"], "Bearer my-token");
    assert_eq!(out.headers["X-Powered-By"], "Rust");

    Ok(())
}

Web API の呼び出し

GitHub API をクエリする

reqwest-badge serde-badge cat-net-badge cat-encoding-badge

GitHub リポジトリにスターを付けたすべてのユーザーの一覧を取得するために、reqwest::get を使用して [GitHub stargazers API v3][github-api-stargazers] をクエリします。 reqwest::Response は、serde::Deserialize を実装した User オブジェクトへデシリアライズされます。

このプログラムでは、GitHub パーソナルアクセストークンが環境変数 GITHUB_TOKEN に指定されていることを 想定しています。リクエストの設定には、[GitHub API][github-api] で必要とされる [reqwest::header::USER_AGENT] ヘッダーが含まれています。プログラムは [serde_json::from_str] を使って レスポンス本文を User オブジェクトのベクターへデシリアライズし、 レスポンスを処理して User インスタンスに変換します。

use serde::Deserialize;
use reqwest::Error;
use reqwest::header::USER_AGENT;

#[derive(Deserialize, Debug)]
struct User {
    login: String,
    id: u32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let request_url = format!("https://api.github.com/repos/{owner}/{repo}/stargazers",
                              owner = "rust-lang-nursery",
                              repo = "rust-cookbook");
    println!("{}", request_url);
    
    let client = reqwest::blocking::Client::new();
    let response = client
        .get(request_url)
        .header(USER_AGENT, "rust-web-api-client") // gh api では user-agent ヘッダーが必要
        .send()?;

    let users: Vec<User> = response.json()?;
    println!("{:?}", users);
    Ok(())
}

API リソースが存在するかどうかを確認する

reqwest-badge cat-net-badge

HEAD リクエスト (Client::head) を使用して GitHub Users Endpoint をクエリし、その後レスポンスコードを調べて成功したかどうかを判定します。これは、ボディを受信することなく REST リソースをクエリするための手早い方法です。ClientBuilder::timeout で設定した reqwest::Client により、リクエストがタイムアウト時間を超えて継続しないことが保証されます。

ClientBuilder::build と [ReqwestBuilder::send] はどちらも reqwest::Error 型を返すため、main 関数の戻り値の型には短縮形の reqwest::Result を使用します。

use reqwest::Result;
use std::time::Duration;
use reqwest::ClientBuilder;

fn main() -> Result<()> {
    let user = "ferris-the-crab";
    let request_url = format!("https://api.github.com/users/{}", user);
    println!("{}", request_url);

    let timeout = Duration::new(5, 0);
    let client = reqwest::blocking::ClientBuilder::new().timeout(timeout).build()?;
    let response = client.head(&request_url).send()?;

    if response.status().is_success() {
        println!("{} is a user!", user);
    } else {
        println!("{} is not a user!", user);
    }

    Ok(())
}

GitHub API を使用して Gist を作成および削除する

reqwest-badge serde-badge cat-net-badge cat-encoding-badge

Client::post を使用して GitHub の [gists API v3][gists-api] に POST リクエストを送信し、Client::delete を使用して DELETE リクエストでそれを削除し、gist を作成します。

reqwest::Client は、URL、ボディ、認証を含む両方のリクエストの詳細を担当します。serde_json::json! マクロによる POST ボディは任意の JSON ボディを提供します。RequestBuilder::json の呼び出しでリクエストボディを設定します。RequestBuilder::basic_auth は認証を処理します。RequestBuilder::send の呼び出しによって、リクエストが同期的に実行されます。

use anyhow::Result;
use serde::Deserialize;
use serde_json::json;
use std::env;
use reqwest::Client;

#[derive(Deserialize, Debug)]
struct Gist {
    id: String,
    html_url: String,
}

fn main() -> Result<()> {
    let gh_user = env::var("GH_USER")?;
    let gh_pass = env::var("GH_PASS")?;

    let gist_body = json!({
        "description": "the description for this gist",
        "public": true,
        "files": {
             "main.rs": {
             "content": r#"fn main() { println!("hello world!");}"#
            }
        }});

    let request_url = "https://api.github.com/gists";
    let response = reqwest::blocking::Client::new()
        .post(request_url)
        .basic_auth(gh_user.clone(), Some(gh_pass.clone()))
        .json(&gist_body)
        .send()?;

    let gist: Gist = response.json()?;
    println!("Created {:?}", gist);

    let request_url = format!("{}/{}",request_url, gist.id);
    let response = reqwest::blocking::Client::new()
        .delete(&request_url)
        .basic_auth(gh_user, Some(gh_pass))
        .send()?;

    println!("Gist {} deleted! Status code: {}",gist.id, response.status());
    Ok(())
}

この例では、GitHub API へのアクセスを認可するために HTTP Basic Auth を使用しています。一般的なユースケースでは、はるかに複雑な OAuth 認可フローのいずれかを使用することになるでしょう。

ページネーションされた RESTful API を利用する

reqwest-badge serde-badge cat-net-badge cat-encoding-badge

ページネーションされた Web API を、使いやすい Rust イテレータでラップします。イテレータは遅延 評価され、各ページの末尾に達すると、リモートサーバーから次の結果ページを取得します。

// cargo-deps: reqwest="0.11", serde="1"
mod paginated {
  use reqwest::Result;
  use reqwest::header::USER_AGENT;
  use serde::Deserialize;

  #[derive(Deserialize)]
  struct ApiResponse {
      dependencies: Vec<Dependency>,
      meta: Meta,
  }

  #[derive(Deserialize)]
  pub struct Dependency {
      pub crate_id: String,
      pub id: u32,
  }

  #[derive(Deserialize)]
  struct Meta {
      total: u32,
  }

  pub struct ReverseDependencies {
      crate_id: String,
      dependencies: <Vec<Dependency> as IntoIterator>::IntoIter,
      client: reqwest::blocking::Client,
      page: u32,
      per_page: u32,
      total: u32,
  }

  impl ReverseDependencies {
      pub fn of(crate_id: &str) -> Result<Self> {
          Ok(ReverseDependencies {
                 crate_id: crate_id.to_owned(),
                 dependencies: vec![].into_iter(),
                 client: reqwest::blocking::Client::new(),
                 page: 0,
                 per_page: 100,
                 total: 0,
             })
      }

      fn try_next(&mut self) -> Result<Option<Dependency>> {
          if let Some(dep) = self.dependencies.next() {
              return Ok(Some(dep));
          }

          if self.page > 0 && self.page * self.per_page >= self.total {
              return Ok(None);
          }

          self.page += 1;
          let url = format!("https://crates.io/api/v1/crates/{}/reverse_dependencies?page={}&per_page={}",
                            self.crate_id,
                            self.page,
                            self.per_page);
          println!("{}", url);

          let response = self.client.get(&url).header(
                     USER_AGENT,
                     "cookbook agent",
                 ).send()?.json::<ApiResponse>()?;
          self.dependencies = response.dependencies.into_iter();
          self.total = response.meta.total;
          Ok(self.dependencies.next())
      }
  }

  impl Iterator for ReverseDependencies {
      type Item = Result<Dependency>;

      fn next(&mut self) -> Option<Self::Item> {
          match self.try_next() {
              Ok(Some(dep)) => Some(Ok(dep)),
              Ok(None) => None,
              Err(err) => Some(Err(err)),
          }
      }
  }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    for dep in paginated::ReverseDependencies::of("serde")? {
        let dependency = dep?;
        println!("{} depends on {}", dependency.id, dependency.crate_id);
    }
    Ok(())
}

ダウンロード

一時ディレクトリにファイルをダウンロードする

reqwest-badge tempfile-badge cat-net-badge cat-filesystem-badge

tempfile::Builder を使って一時ディレクトリを作成し、reqwest::get を使用して HTTP 経由でファイルを非同期にダウンロードします。

Response::url から取得した名前で、tempfile::TempDir::path 配下に対象の File を作成し、ダウンロードしたデータを io::copy でそこにコピーします。 一時ディレクトリは、プログラムの終了時に自動的に削除されます。

use anyhow::Result;
use std::io::Write;
use std::fs::File;
use tempfile::Builder;

fn main() -> Result<()> {
    let tmp_dir = Builder::new().prefix("example").tempdir()?;
    let target = "https://www.rust-lang.org/logos/rust-logo-512x512.png";
    let response = reqwest::blocking::get(target)?;

    let mut dest = {
        let fname = response
            .url()
            .path_segments()
            .and_then(|segments| segments.last())
            .and_then(|name| if name.is_empty() { None } else { Some(name) })
            .unwrap_or("tmp.bin");

        println!("file to download: '{}'", fname);
        let fname = tmp_dir.path().join(fname);
        println!("will be located under: '{:?}'", fname);
        File::create(fname)?
    };
    let content =  response.bytes()?;
    dest.write_all(&content)?;
    Ok(())
}

paste-rs にファイルを POST する

reqwest-badge cat-net-badge

reqwest::Client は、reqwest::RequestBuilder パターンに従って https://paste.rs への接続を確立します。URL を指定して Client::post を呼び出すと 送信先が設定され、RequestBuilder::body はファイルを読み取って送信する内容を設定し、 RequestBuilder::send はファイルのアップロードが完了してレスポンスが返るまで ブロックします。read_to_string はサーバーのレスポンスからメッセージを返し、 コンソールに表示します。

use anyhow::Result;
use std::fs::File;
use std::io::Read;

fn main() -> Result<()> {
    let paste_api = "https://paste.rs";
    let mut file = File::open("message")?;

    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    let client = reqwest::blocking::Client::new();
    let res = client.post(paste_api)
        .body(contents)
        .send()?;
    let response_text = res.text()?;
    println!("Your paste is located at: {}",response_text );
    Ok(())
}

HTTP Range ヘッダーを使って部分ダウンロードを行う

reqwest-badge cat-net-badge

レスポンスの Content-Length を取得するために reqwest::blocking::Client::head を使用します。

続いて、コードは reqwest::blocking::Client::get を使用して、進捗メッセージを表示しながらコンテンツを 10240 バイト単位のチャンクでダウンロードします。このアプローチは、大きなファイルのメモリ使用量を抑えるのに役立ち、ダウンロードの再開も可能にします。

Range ヘッダーは RFC7233 で定義されています。

use anyhow::{Result, anyhow};
use reqwest::header::{HeaderValue, CONTENT_LENGTH, RANGE};
use reqwest::StatusCode;
use std::fs::File;
use std::str::FromStr;

struct PartialRangeIter {
  start: u64,
  end: u64,
  buffer_size: u32,
}

impl PartialRangeIter {
  pub fn new(start: u64, end: u64, buffer_size: u32) -> Result<Self> {
    if buffer_size == 0 {
      return Err(anyhow!("invalid buffer_size, give a value greater than zero."));
    }
    Ok(PartialRangeIter {
      start,
      end,
      buffer_size,
    })
  }
}

impl Iterator for PartialRangeIter {
  type Item = HeaderValue;
  fn next(&mut self) -> Option<Self::Item> {
    if self.start > self.end {
      None
    } else {
      let prev_start = self.start;
      self.start += std::cmp::min(self.buffer_size as u64, self.end - self.start + 1);
      Some(HeaderValue::from_str(&format!("bytes={}-{}", prev_start, self.start - 1)).expect("string provided by format!"))
    }
  }
}

fn main() -> Result<()> {
  let url = "https://httpbin.org/range/102400?duration=2";
  const CHUNK_SIZE: u32 = 10240;
    
  let client = reqwest::blocking::Client::new();
  let response = client.head(url).send()?;
  let length = response
    .headers()
    .get(CONTENT_LENGTH)
    .ok_or_else(|| anyhow!("response doesn't include the content length"))?;
  let length = u64::from_str(length.to_str()?).map_err(|_| anyhow!("invalid Content-Length header"))?;
    
  let mut output_file = File::create("download.bin")?;
    
  println!("starting download...");
  for range in PartialRangeIter::new(0, length - 1, CHUNK_SIZE)? {
    println!("range {:?}", range);
    let mut response = client.get(url).header(RANGE, range).send()?;
    
    let status = response.status();
    if !(status == StatusCode::OK || status == StatusCode::PARTIAL_CONTENT) {
      return Err(anyhow!("Unexpected server response: {}", status));
    }
    std::io::copy(&mut response, &mut output_file)?;
  }
    
  println!("Finished with success!");
  Ok(())
}

認証

基本認証

reqwest-badge cat-net-badge

reqwest::RequestBuilder::basic_auth を使用して、HTTP の基本認証を実行します。

use reqwest::blocking::Client;
use reqwest::Error;

fn main() -> Result<(), Error> {
    let client = Client::new();

    let user_name = "testuser".to_string();
    let password: Option<String> = None;

    let response = client
        .get("https://httpbin.org/")
        .basic_auth(user_name, password)
        .send();

    println!("{:?}", response);

    Ok(())
}

フルスタックWeb

Leptos はフルスタックWebフレームワークです。最初のレシピでは、サーバーのみで HTML をレンダリングします。2 つ目ではハイドレーションを追加し、クライアントが同じコンポーネントを WebAssembly として再実行してインタラクティブ性を提供できるようにします。

フィルタリングした結果を HTML として返す

leptos-badge leptos-router-badge leptos-axum-badge axum-badge tokio-badge cat-net-badge

このクレートは、ssr フィーチャーを有効にした leptos に加えて、leptos_routerleptos_axumaxumtokio を使用します。leptos には resolver = "3" が必要なため、メインのワークスペースの外にあります。

ルーターを定義する

Router は、ブラウザーがアプリにアクセスしたときにどのページを表示するかを担当します。path! マクロは、どの URL にマッチするかを定義するために使われます。:query はテンプレートで、任意の値にマッチし、その値をコンポーネントが後で読み取れるよう query として保存します。

#[component]
fn App() -> impl IntoView {
    view! {
        <LeptosRouter>
            <nav><A href="/">"Home"</A>" | "<A href="/filter/serde">"serde"</A>" | "<A href="/filter/tokio">"tokio"</A></nav>
            <main>
                <Routes fallback=|| "Not found.">
                    <Route path=path!("") view=HomePage />
                    <Route path=path!("/filter/:query") view=FilterPage />
                </Routes>
            </main>
        </LeptosRouter>
    }
}

<A> はリンクのアンカーとなり、<Routes fallback=...> はデフォルトを提供するために使われます。

詳しくは Leptos book の <Routes/> の定義ネストされたルーティング を参照してください。

パラメーターを読み取り、サーバーで取得する

use_paramsRouter からの値を保持します。Resource::new は、非同期に取得するためにクロージャを使います。このクロージャはサーバーで実行され、<Suspense> はサーバーが処理中であることをユーザーに伝えます。

#[derive(Params, PartialEq, Clone, Debug)]
struct FilterParams {
    query: Option<String>,
}

#[component]
fn FilterPage() -> impl IntoView {
    let params = use_params::<FilterParams>();
    let query = move || {
        params
            .read()
            .as_ref()
            .ok()
            .and_then(|p| p.query.clone())
            .unwrap_or_default()
    };

    let results = Resource::new(query, filter_crates);

    view! {
        <h1>"Results"</h1>
        <Suspense fallback=|| view! { <p>"Loading..."</p> }>
            {move || results.get().map(|r| match r {
                Ok(items) => view! { <CrateList items /> }.into_any(),
                Err(err) => view! { <p>{format!("error: {err}")}</p> }.into_any(),
            })}
        </Suspense>
    }
}

filter_crates は、サーバーで実行される単純な async fn です。データファイルは env!("CARGO_MANIFEST_DIR") を介して解決されるため、バイナリをどこから実行してもパスが機能します。

async fn filter_crates(query: String) -> Result<Vec<String>, String> {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("data/crates.txt");
    let text = tokio::fs::read_to_string(path)
        .await
        .map_err(|e| e.to_string())?;
    let q = query.to_lowercase();
    Ok(text
        .lines()
        .filter(|line| line.to_lowercase().contains(&q))
        .map(String::from)
        .collect())
}

詳しくは Leptos book の パラメーターとクエリResource を使ったデータの読み込みSuspense を参照してください。

再利用可能なコンポーネントを書く

コンポーネントは #[component] が付いた関数です。Props は通常の関数引数です。

#[component]
fn CrateList(items: Vec<String>) -> impl IntoView {
    view! {
        <p>{format!("{} match(es)", items.len())}</p>
        <ul>
            {items.into_iter().map(|name| view! { <li>{name}</li> }).collect::<Vec<_>>()}
        </ul>
    }
}

詳しくは Leptos book の コンポーネントと Propsコンポーネントに Children を渡す を参照してください。

Axum を組み込む

generate_route_list(App)Routes ツリーをたどって、ルーターが認識しているすべてのパスを列挙します。leptos_routes はそれらのパスを、各リクエストでシェルをレンダリングする Axum の GET ハンドラーとして登録します。

fn shell(_options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html>
            <head>
                <meta charset="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <title>"Crate Filter"</title>
            </head>
            <body><App /></body>
        </html>
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr: std::net::SocketAddr = "127.0.0.1:3000".parse()?;
    let conf = LeptosOptions::builder()
        .output_name("web_leptos")
        .site_addr(addr)
        .build();
    let routes = generate_route_list(App);

    let app = Router::new()
        .leptos_routes(&conf, routes, {
            let opts = conf.clone();
            move || shell(opts.clone())
        })
        .with_state(conf);

    let listener = tokio::net::TcpListener::bind(&addr).await?;
    println!("listening on http://{addr}");
    axum::serve(listener, app).await?;
    Ok(())
}

詳しくは Leptos book の ページロードのライフサイクルcargo-leptos(多くの実アプリが最終的に移行するツール)を参照してください。

実行:

cargo run --manifest-path crates/web_leptos/Cargo.toml
# http://127.0.0.1:3000/
# http://127.0.0.1:3000/filter/serde
# http://127.0.0.1:3000/filter/tokio

コンポーネントの状態をサーバーと同期する

leptos-badge leptos-axum-badge axum-badge tokio-badge wasm-bindgen-badge cat-net-badge

最初のレシピと同じ data/crates.txt を使いますが、ユーザーはテキストボックスに入力し、結果は入力に応じて更新されます。ページの再読み込みはありません。ファイルはサーバー上に置かれたままで、クエリはサーバー関数を経由して往復します。

このレシピでは、ビルドツールとして cargo-leptos、ターゲットとして wasm32-unknown-unknown を使います:

cargo install cargo-leptos
rustup target add wasm32-unknown-unknown

feature flag で ssrhydrate を分ける

1 つのクレートで、ビルドは 2 種類です。ssr はサーバー側の依存関係を取り込み、hydrate は WASM クライアントを有効にします。cargo-leptos はその両方を [package.metadata.leptos] から制御します。

crate-type = ["cdylib", "rlib"]

[dependencies]
leptos = "0.8.19"
leptos_axum = { version = "0.8.9", optional = true }
leptos_router = "0.8.13"
axum = { version = "0.8", optional = true }
tokio = { version = "1.42", features = ["rt-multi-thread", "macros"], optional = true }
tower-http = "0.6"
wasm-bindgen = "=0.2.118"
console_error_panic_hook = "0.1"

[dev-dependencies]
cargo-leptos = "0.3.6"

[features]
hydrate = ["leptos/hydrate"]
ssr = [
    "dep:leptos_axum",
    "dep:axum",
    "dep:tokio",
    "leptos/ssr",
    "leptos_router/ssr",
]

[package.metadata.leptos]
output-name = "web_leptos_hydrate"
site-root = "target/site"
site-pkg-dir = "pkg"
site-addr = "127.0.0.1:3000"
reload-port = 3001
bin-features = ["ssr"]

フィルター処理をサーバー関数にする

#[server]filter_crates をクライアントから呼び出せる関数にします。本体は ssr ビルドにのみコンパイルされ、hydrate ではマクロがサーバーに POST するスタブを残します。tokio::fs は、これまでと同様にサーバー上でファイルを読み取ります。

#[server]
pub async fn filter_crates(query: String) -> Result<Vec<String>, ServerFnError> {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("data/crates.txt");
    let text = tokio::fs::read_to_string(path).await?;
    let q = query.to_lowercase();
    Ok(text
        .lines()
        .filter(|line| line.to_lowercase().contains(&q))
        .map(String::from)
        .collect())
}

入力をリソースにバインドする

App コンポーネントは、ssr にも hydrate にも分岐しない、通常のリアクティブな Leptos コンポーネントです。signal がクエリを保持し、event_target_value が各キーストロークから文字列を取り出し、Resource::new がクエリが変わるたびに filter_crates を再実行します。<Suspense> は結果、または「検索中…」のフォールバックを描画します。

#[component]
pub fn App() -> impl IntoView {
    let (query, set_query) = signal(String::new());
    let results = Resource::new(
        move || query.get(),
        |q| async move { filter_crates(q).await },
    );

    view! {
        <h1>"Crate search"</h1>
        <input
            type="text"
            placeholder="Type to filter..."
            prop:value=query
            on:input=move |ev| set_query.set(event_target_value(&ev))
        />
        <Suspense fallback=|| view! { <p>"Searching..."</p> }>
            {move || results.get().map(|r| match r {
                Ok(items) => view! {
                    <p>{format!("{} match(es)", items.len())}</p>
                    <ul>
                        {items.into_iter().map(|name| view! { <li>{name}</li> }).collect::<Vec<_>>()}
                    </ul>
                }.into_any(),
                Err(err) => view! { <p>{format!("error: {err}")}</p> }.into_any(),
            })}
        </Suspense>
    }
}

サーバー側: HydrationScripts でシェルをレンダリングする

<HydrationScripts> は、WASM バンドルを読み込む <script> タグを出力します。<AutoReload> は、cargo leptos watch の実行中に再読み込みスクリプトを提供します。

#[cfg(feature = "ssr")]
pub fn shell(options: leptos::config::LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html>
            <head>
                <meta charset="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <AutoReload options=options.clone() />
                <HydrationScripts options />
                <title>"Crate search"</title>
            </head>
            <body><App /></body>
        </html>
    }
}

クライアント側: body を hydrate する

hydrate_body はブラウザで同じ App 関数を実行し、 それをサーバーでレンダリングされた DOM にアタッチします。#[wasm_bindgen] は、 生成された JS が呼び出すエントリポイントとしてこれを示します。

#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
    console_error_panic_hook::set_once();
    leptos::mount::hydrate_body(App);
}

ssr フィーチャーの背後で Axum を接続する

get_configuration[package.metadata.leptos] セクションを読み取ります。leptos_routes は サーバー関数のエンドポイントも自動的に登録します。

#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    use axum::Router;
    use leptos::config::get_configuration;
    use leptos_axum::{LeptosRoutes, generate_route_list};
    use std::path::Path;
    use tower_http::services::ServeDir;
    use web_leptos_hydrate::{App, shell};

    let conf = get_configuration(None)?;
    let options = conf.leptos_options;
    let addr = options.site_addr;
    let routes = generate_route_list(App);

    let app = Router::new()
        .leptos_routes(&options, routes, {
            let opts = options.clone();
            move || shell(opts.clone())
        })
        .nest_service("/pkg", ServeDir::new(Path::new(&*options.site_root).join("pkg")))
        .with_state(options);

    let listener = tokio::net::TcpListener::bind(&addr).await?;
    println!("listening on http://{addr}");
    axum::serve(listener, app).await?;
    Ok(())
}

#[cfg(not(feature = "ssr"))]
fn main() {}

実行します:

cd crates/web_leptos_hydrate
cargo leptos watch
# http://127.0.0.1:3000/

詳細は Leptos book の Server Functionscargo-leptos を参照してください。