Cargoパッケージの例
ここではcrates.ioで配布されている、Rustでよく使用されるパッケージの例を示します。
注意
ここで紹介するパッケージはRustと同様にコミュニティによって活発に開発されているオープンソースソフトウェアです。各パッケージの執筆時点の最新版でSOLID-Rust上での動作を確認しており、この状態を維持するよう務めておりますが、動作の継続的な保証はいたしかねます。
libc
libc はターゲットプラットフォームのシステムライブラリ (C言語ライブラリ関数が主) のFFI関数・型宣言を提供するパッケージです。libc 0.2.104以降にSOLIDターゲットのサポートが含まれています。
[dependencies]
libc = "0.2"
let mut buf = [0u8; 32];
unsafe { libc::memset(buf.as_mut_ptr().cast(), 5, 32) };
assert_eq!(buf, [5; 32]);
cc
ビルドスクリプト [The Cargo Book] はCargoパッケージをビルドする前に自動的に実行されるRustプログラムです。 cc パッケージをCargoパッケージのビルドスクリプト内で使用することで、C/C++ライブラリをビルド・リンクすることができます。cc 1.0.71以降にSOLIDターゲットのサポートが含まれています。
cc は zstd-sys パッケージのビルドスクリプトなどで使用されています。
Note
cc はビルドしたC/C++ライブラリを自動的にリンクすることができますが、このリンクされたライブラリとRustコードを結びつけるのに必要なFFI記述は生成しません。
rand
rand は乱数生成に関係するAPIを提供するパッケージです。
[dependencies]
rand = "0.8"
一番簡単で多用途なAPIは rand::thread_rng()
と rand::random()
です。これらの関数はrandがスレッド単位で管理するセキュアな疑似乱数生成器へのアクセスを提供します。この疑似乱数生成器は getrandom ライブラリを使用してシードされます。
use rand::prelude::*;
if rand::random() { // 真偽値を生成
println!("hi!");
}
println!("rand i32 = {}", rand::random::<i32>());
// スレッドローカル乱数生成器への参照を保持
let mut rng = rand::thread_rng();
println!("rand f32 = {}", rng.gen::<f32>());
println!("rand in {{x | 0 ≤ x < 1000}} = {}", rng.gen_range(0..1000));
let mut nums: Vec<i32> = (1..100).collect();
nums.shuffle(&mut rng);
println!("nums = {nums:?}");
Note
最適化レベル0ではスタックを異常に消費するようです。次の記述 [The Cargo Book] を Cargo.toml
ファイルに追加し、デバッグビルドの最適化レベルをオーバライドすることを推奨します。
[profile.dev.package]
rand = { opt-level = 1 }
速度が重要な場合には rand::rngs::SmallRng
が便利です。現在の実装ではXoshiro128/256++が使用されています。
let mut values = vec![0u32; 1024 * 1024];
let mut rng = rand::rngs::SmallRng::from_entropy();
dbg!(time(|| values.fill_with(|| rng.gen()))); // 9.44ms
let mut rng = rand::thread_rng();
dbg!(time(|| values.fill_with(|| rng.gen()))); // 71.61ms
#[inline(never)]
fn time(mut f: impl FnMut()) -> std::time::Duration {
f(); // warm up
let t = std::time::Instant::now();
f();
t.elapsed()
}
getrandom
getrandom はターゲットプラットフォームのセキュアランダムデータ生成APIを抽象化したプラットフォーム非依存APIを提供するパッケージです。getrandom 0.2.4以降にSOLIDターゲットのサポートが含まれています。randのエントロピー源として使用されています。
[dependencies]
getrandom = "0.2"
let mut buf = [0u8; 32];
getrandom::getrandom(&mut buf).unwrap();
println!("got random bytes: {buf:02x?}");
log
log はRustで幅広く使用されているロギングフレームワークの一つです。info!
や warn!
等のマクロを使用してメッセージを出力します。書式は標準ライブラリのフォーマット機能 (std::fmt
) で使用しているものと同じです。
メッセージの出力先 (ロガー, log::Log
) は別途登録する必要があり、env_logger や dumb_logger などのパッケージが提供しています。
[dependencies]
log = "0.4"
env_logger = { version = "0.8", default-features = false }
// ロガーを登録
env_logger::Builder::new()
.filter_level(log::LevelFilter::Trace)
.init();
let x = "message";
// メッセージを出力
log::debug!("this is a debug {x}");
log::error!("this is an error {msg}", msg = "message");
ログレベルは次の方法で設定できます。
logパッケージのCargo featuresを使用することで最大ログレベルを静的に設定できます。例えば、
dependencies.log
を次のように書き換えることで、リリースビルド時はerror!
とwarn!
、デバッグビルド時はそれに加えてinfo!
とdebug!
を表示するようにできます。[dependencies] log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
ロガーが実行時に最大ログレベルを設定する機能を有していることがあります。例えば、上の例では
env_logger::Builder::filter_level()
を使用してログレベルを設定しています。
Note
上記のロガーは標準ライブラリの標準出力にメッセージを出力します。
別のロギングフレームワークとしてより高機能な tracing があります。logからtracingにログを変換するアダプタ tracing_log も提供されています。
serde
serde はRustのデータ構造をシリアライズ・デシリアライズするためのフレームワークです。 serde_json 等のシリアライザと併せて使用します。
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Measurement {
temperature: f32,
humidity: f32,
}
let m = Measurement { temperature: 20.0, humidity: 50.0 };
// JSON形式にシリアライズ
let json_str = serde_json::to_string(&m).unwrap();
// → {"temperature":20.0,"humidity":50.0}
println!("serialized = {json_str}");
// JSON形式からデシリアライズ
let m: Measurement = serde_json::from_str(&json_str).unwrap();
// → Measurement { temperature: 20.0, humidity: 50.0 }
println!("deserialized = {m:?}");
rayon
rayon はデータ並列ライブラリです。Rust標準ライブラリのイテレータと似たような記法を使用しながら、処理を複数のコアに分散させることが簡単かつ安全にできます。
[dependencies]
rayon = "1"
use rayon::prelude::*;
// Rayonワーカスレッドを起動する
rayon::ThreadPoolBuilder::new()
.num_threads(4) // スレッド数
.start_handler(|i| unsafe {
// ワーカスレッドを各コアに分配する
assert_eq!(itron::abi::mig_tsk(itron::abi::TSK_SELF, i as i32), 0);
})
.build_global()
.unwrap();
let mut values: Vec<_> = (0..1_000_000).map(|x| x as f64).collect();
// 標準ライブラリのイテレータを使用して計算する
dbg!(time(|| values.iter().map(|x| x.powf(0.1)).sum::<f64>())); // 448.544ms
// Rayonを使用して計算する
dbg!(time(|| values.par_iter().map(|x| x.powf(0.1)).sum::<f64>())); // 114.987ms
fn time<R: std::fmt::Debug>(mut f: impl FnMut() -> R) -> std::time::Duration {
f(); // warm up
let t = std::time::Instant::now();
dbg!(f());
t.elapsed()
}
Note
上記例では itron パッケージが提供するFFI関数を使用しています。このパッケージの使用方法についてはチュートリアル TOPPERS APIを使用する (方法2) を参照してください。
spin
spin はアトミック操作とスピンロックに基づいた同期プリミティブを提供します。
TOPPERSカーネルのような優先度ベーススケジューリングに基づいたRTOSカーネルではスピンロックは他のタスクの実行を無期限に妨げるため、同コア内のタスク間の排他処理にはあまり有用ではありません。しかし、ロックの取得失敗時にエラーを返す種類のメソッド (e.g., spin::Mutex::try_lock
) のみを使用した場合、実質的に std::cell::RefCell
のスレッド安全バージョンとして使用できます。このような用途の場合、タスクを待ち状態に遷移させることができる std::sync::Mutex
等より遥かに効率的で、またカーネル資源を消費しません。
[dependencies]
spin = "0.9"
let mut cells = Vec::new();
dbg!(time(|| cells.extend((0..1000).map(|_| std::sync::Mutex::new(0))))); // 196µs
dbg!(time(|| cells.iter().for_each(|c| *c.lock().unwrap() += 1))); // 286µs
let mut cells = Vec::new();
dbg!(time(|| cells.extend((0..1000).map(|_| spin::Mutex::new(0))))); // 3μs
dbg!(time(|| cells.iter().for_each(|c| *c.try_lock().unwrap() += 1))); // 56μs
fn time(f: impl FnOnce()) -> std::time::Duration {
let t = std::time::Instant::now();
f();
t.elapsed()
}
std::sync::Mutex
と同様に spin::Mutex
は定数コンテキストで生成できるため、グローバル変数に置くことができます。
struct GlobalState {
x: i32,
}
static GLOBAL_STATE: spin::Mutex<GlobalState> = spin::Mutex::new(GlobalState {
x: 42,
});
#[no_mangle]
extern "C" fn root_task(_: usize) {
let mut gs = GLOBAL_STATE.try_lock().unwrap();
gs.x += 1;
assert_eq!(gs.x, 43);
}