項目33:ライブラリコードをno_std互換にすることを検討しよう
https://effective-rust.com/no-std.html
LT;DR
std クレートの多くは、core や alloc で定義されている
no_std 互換であることを CI で保証しよう
ヒープ環境が限定されている(メモリ確保が失敗し得る)環境で Rust を動作させることは、ライブラリのサポートが限定的であることを知っておこう
hr.icon
std クレート の一部は、 プレリュード によって自動的にインポートされる
e.g. std::vec::Vec
Rust は std が提供できない環境に向けたコードのビルドもサポートしている
e.g. ブートローダ や ファームウェア などの 組み込み 環境
このような環境でビルドされるクレートは、src/lib.rs の トップレベル に、#![no_std] を付与する
core
制限の強いプラットフォームでも、標準ライブラリの多くの基本的な型(e.g. Option や Result)は利用できる
名前が異なる(core:: から始まる)が、std:: の型と同じように動作する
∵ 実際の型は同じ型であり、std:: の型は core:: の型を再エクスポートしているため
core の型は自動的に使えるが、no_std 環境下では明示的にインポート(use)する必要がある
core では、「ヒープ 上に メモリ を確保しない」
そのため、ベクタやマップ、セットなどは提供していない
alloc
ヒープなどにメモリを確保するための機能を提供するクレート
core と同様、alloc:: の型は std:: のものと同様に動作する
∵ std:: の型は alloc:: の型を再エクスポートしているため
e.g. std::vec::Vec は alloc::vec::Vec を再エクスポートしている
no_std なクレートで利用する際には、extern crate alloc; で利用することを明示する必要がある
alloc で提供されているアイテム(一部)
alloc::boxed::Box<T>
alloc::rc::Rc<T>
alloc::sync::Arc<T>
alloc::vec::Vec<T>
alloc::string::String
alloc::format!
alloc::collections::BTreeMap<K, V>
alloc::collections::BTreeSet<T>
warning.icon
HashMap と HashSet は存在しない
ハッシュ衝突攻撃 を防ぐために、これらのデータ構造は 乱数シード に依存しており、安全な乱数生成には OS が必要であるため
そのため、no_std では BTreeMap や BTreeSet を用いる と良い
std::sync::Mutex などの同期機構はない
OS 固有の 同期プリミティブ に依存しているため
そのため、no_std でマルチスレッドセーフなコードを書く場合は、spin-rs などのサードパーティ製のライブラリを用いる と良い
no_std に対するコードを書く
あるライブラリクレートを no_std 互換にする場合、依存ライブラリも no_std 互換にする必要がある
しかし、コンパイラは std 依存の依存ライブラリがあっても何も警告を出さない
したがって、「no_std のビルドのチェックを CI に追加しよう」
具体的には、ビルドターゲット を std をサポートしていないもの(e.g. --target-thumbv6m-none-eabi)にして、クロスコンパイル すれば良い
以上のような軽微な対応で no_std 互換にできる場合、再利用性を高めるために「no_std 互換にすることを検討しよう」
no_std 互換にできない部分が一部であるなら、クレートに フィーチャ を追加して、その部分だけ std とすれば良い
このようなフィーチャ名は慣習として、std 固有の機能を使う場合は std
#![cfg_attr(not(feature = "std"), no_std)]
alloc 固有の機能を使う場合は alloc とする
code:rs
#cfg(feature = "alloc"
extern crate alloc;
warning.icon
no_std フィーチャは std の機能を完全に無効にするわけではないので、加法的 であるようにすべき
項目26:忍び寄るフィーチャに注意しよう
ユーザがあるクレートのフィーチャに no_std を指定しても、依存ライブラリが同じクレートを std で指定していた場合、どちらも有効になる
これを実現するには、「CI で全ての組み合わせをビルドするように設定しよう」
失敗の可能性があるメモリ確保
ヒープ上のメモリ確保は可能だが、ヒープの容量が限定されて確保に失敗し得る環境も存在する
しかし、alloc はヒープ上のメモリ確保は失敗しないことを前提としている
この前提により、「通常」の環境ではコードは書きやすくなっている
通常メモリは事実上無制限であり、枯渇した場合は(プログラムの問題ではなく)システム全体が他の大きな問題を抱えていることを示している
一方、メモリ量が制限されている環境には適しておらず、むしろ古い言語の方が適している
C: メモリ確保を手動(malloc が NULL を返すか)で行う
C++: 例外 機構を用いて、std::bad_alloc 例外でメモリ確保の失敗を捕捉できる
失敗した場合の挙動は ツールチェーン や ビルドターゲット、設定によって変わるが、多くの場合は panic! が発生してプログラムは停止する
Rust がメモリ確保の失敗に対応できないことは、様々なところで指摘されたため、現在では回避策が用意されている
様々なところで指摘
Linux カーネル
Android
curl
回避策
失敗する可能性のある API に Result<_, AllocError> を返す新しい代替メソッド(try_*)の追加
e.g.
Vec::reverse → Vec::try_reverse
Box::new → Box::try_new
ただし、まだほとんど用意されていないので、たとえば Vec::push などは自前で用意する必要がある
code:rs
fn try_build_a_vec() -> Result<Vec<u8>, String> {
let mut v = Vec::new();
// サイズをここでは単純に 4 としている
// しかし、本来は無駄なメモリ確保や不足による失敗を避けるために、慎重に決定すべき
let required_size = 4;
// try_reverse で、成功すれば確実にメモリ確保されることを保証
v.try_reverse(required_size)
.map_err(|_e| format!("Failed to allocate {} items!", required_size))?;
v.push(1);
v.push(2);
v.push(3);
v.push(4);
Ok(v)
}
no_global_oom_handling
オフに設定することで、失敗しないことを前提としているメモリ確保操作を無効にできる
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目