標準ライブラリなしのRust:no_std開発の深掘り
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Rustプログラミングの活気ある世界では、データ構造からネットワーキング機能まで、あらゆるものを提供する豊富なエコシステムと強力なstd
ライブラリを当然のこととして受け入れていることがよくあります。それは、数え切れないほどのアプリケーションの開発を加速させる、快適で高レベルな環境です。しかし、すべてのコンピューティング環境がそのような贅沢を提供しているわけではありません。メモリが極端に制限されているシステム、オペレーティングシステムがない、または厳格なリアルタイム制約がある――組み込みデバイス、マイクロコントローラー、あるいはオペレーティングシステムカーネルのまさにコア――などを想像してみてください。これらのシナリオでは、OSサービスと動的メモリ割り当てに依存するstd
ライブラリは、助けになるどころか障害となります。ここで、Rustのno_std
プログラミングが輝くのです。これにより、開発者は非常に効率的なベアメタルコードを記述し、Rustの安全性とパフォーマンス保証を真に制約された環境にまで拡張できます。この記事では、no_std
の刺激的な領域に深く入り込み、その基本を説明し、その適用を実証し、なぜそれがますます多くの開発者にとって不可欠なツールなのかを示します。
no_stdの必須要素
no_std
の旅に出る前に、このプログラミングパラダイムの基盤となる主要な概念を明確に理解しましょう。
主要な用語
no_std
: クレートレベル(#![no_std]
)で適用されるこの属性は、Rustコンパイラに標準ライブラリにリンクしないように伝えます。代わりに、Option
、Result
、基本的な整数型と浮動小数点型、イテレータ、スライスなどの基本的な言語プリミティブを提供するcore
ライブラリにリンクしますが、重要なのはOS依存の機能や動的メモリ割り当てはありません。std
ライブラリ: ファイルI/O、ネットワーキング、スレッディング、コレクション(Vec
やHashMap
など)、動的メモリ管理を含む、一般的なプログラミングタスクのための豊富なAPIセットを提供するRustの標準ライブラリ。core
ライブラリ: すべてのRustプログラム、no_std
プログラムでさえ必要とされる、Rustの基盤となるライブラリ。プリミティブ型、基本的なトレイト、基本的なエラー処理を含む、Rustが機能するために絶対に必要な最小限のものを含んでいます。alloc
クレート:std
ライブラリに依存せずに、Vec
やHashMap
などの一般的なコレクション型を提供するオプションのクレートですが、グローバルアロケータへの依存関係があります。これは、アロケータを提供すれば、no_std
環境でこれらの動的データ構造を使用できることを意味します。- アロケータ: 動的メモリを管理するメカニズム。
std
環境では、デフォルトのシステムアロケータが暗黙的に使用されます。alloc
を使用したno_std
では、グローバルアロケータを明示的に指定して登録する必要があります。 - パニックハンドラ: Rustプログラムが回復不能なエラー(例:配列アクセスの範囲外)に遭遇すると、「パニック」します。
std
環境では、これは通常、バックトレースを表示して終了します。no_std
では、OSがパニックをキャッチしたり表示したりする手段がないため、独自のパニックハンドラを定義する必要があります。 - エントリポイント: プログラムの開始点。
std
プログラムでは、通常はmain
関数です。no_std
環境、特にベアメタル環境では、main
関数または同等の関数を呼び出す前に初期設定を実行するために、リンカースクリプトでリンクされるカスタムエントリポイントを定義する必要があることがよくあります。
原則と実装
no_std
の核心原則は自己完結性です。標準ライブラリがない場合、リソースの管理、エラーの処理、ハードウェアへの直接的な、または特別なHAL(Hardware Abstraction Layers)を介した対話はすべてあなたの責任です。
ここでは、最初に印刷機能なしで、no_std
環境向けのシンプルな「Hello, World!」で例を示します。
#![no_std] // 重要:標準ライブラリをオプトアウトします #![no_main] // 重要:標準のmain関数をオプトアウトします use core::panic::PanicInfo; // カスタムエントリポイントを定義します // `cortex-m-rt`クレートは、ARMマイクロコントローラー向けのより堅牢なエントリポイントを提供することがよくあります。 // 純粋な説明目的のために、ここでは手動で行っています。 #[no_mangle] // リンカがこの関数を名前で見つけられるようにします pub extern "C" fn _start() -> ! { // 初期化コード // 実際の組み込みシステムでは、これはクロック、GPIOなどを構成する可能性があります。 loop { // 私たちのプログラムは無限ループする以外何もしません } } // 独自のパニックハンドラを定義します #[panic_handler] fn panic(_info: &PanicInfo) -> ! { // 実際のアプリケーションでは、これは以下を行う可能性があります: // - エラーを示すためにLEDを点灯させる // - シリアルポートにエラー情報をログに記録する // - システムリセットをトリガーする loop {} }
この最小限の例は、no_std
の最も重要な2つの側面、#![no_std]
とカスタムパニックハンドラを示しています。_start
関数はプログラムのエントリポイントとして機能し、通常は特定のターゲットのリンカースクリプトを介して構成されます。
alloc
クレートの使用
no_std
環境で動的なコレクションが必要な場合は、alloc
を再導入できます。これには2つのこと、Cargo.toml
でalloc
機能を有効にすることと、グローバルアロケータを提供することが必要です。
Cargo.toml
:
[dependencies] # ... その他の依存関係 ... alloc = { version = "0.0.0", package = "alloc" } # `alloc`クレートを使用します。通常は「組み込み」ですが、機能で有効になります。
実際には、alloc
はCargo.toml
に同様の方法で追加する別のクレートではありません。これはRustコンパイラ自体の条件付きコンパイルターゲットです。no_std
プロジェクトでalloc
を有効にするには、通常、これを処理するビルドツールまたはライブラリに依存します。たとえば、cortex-m-alloc
を使用する組み込みプロジェクトでは、その特定のアロケータクレートでalloc
機能を有効にします。一般的な組み込みシステムのパターンを使用しましょう。
cortex-m-alloc
を使用した例:
# Cargo.toml [dependencies] cortex-m = { version = "0.7.6", features = ["critical-section"] } cortex-m-rt = "0.7.0" cortex-m-alloc = "0.4.0" # 選択したアロケータ
src/main.rs
(またはライブラリの場合はsrc/lib.rs
):
#![no_std] #![no_main] #![feature(alloc_error_handler)] // カスタムallocエラーハンドラに必要 extern crate alloc; // `alloc`クレートをスコープに持ち込む use core::panic::PanicInfo; use alloc::vec::Vec; // これでVec!を使用できます // グローバルアロケータを定義する #[global_allocator] static ALLOCATOR: cortex_m_alloc::CortexMHeap = cortex_m_alloc::CortexMHeap::empty(); // アロケータの初期化 // これは通常、`_start`ルーチンで、すべてのアロケーションの前に行われます。 // 簡単にするために、ここではセットアップ関数に入れています。 fn init_allocator() { // メモリ領域でヒープを初期化します // 実際のプログラムでは、このメモリ領域はリンカースクリプトで定義されるか、 // 静的配列になります。 const HEAP_SIZE: usize = 1024; // 1KBヒープ static mut HEAP_MEM: [u8; HEAP_SIZE] = [0; HEAP_SIZE]; // SAFETY:静的変数への可変参照を取得し、アロケータを一度だけ初期化します。 unsafe { ALLOCATOR.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) } } // カスタムエントリポイント #[cortex_m_rt::entry] // ARMマイクロコントローラー用のcortex-m-rtによって提供されます fn main() -> ! { init_allocator(); // アロケータを初期化します let mut my_vec: Vec<u32> = Vec::new(); my_vec.push(10); my_vec.push(20); // もし印刷する方法があれば、ここでmy_vecを印刷します。 // 例えば、シリアルポート経由で送信することによって。 loop { // アプリケーションコード } } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } // メモリ不足エラーのためのカスタムエラーハンドラを定義します #[alloc_error_handler] fn oom(_: core::alloc::Layout) -> ! { // メモリ不足エラーを処理します // 例:LEDを点滅させる、システムをリセットする loop {} }
この例は、動的なアロケーションを導入する方法を示しています。重要なのは#[global_allocator]
を定義し、init
関数を提供して、どこからメモリを管理するかを伝えることです。実際のメモリ領域(HEAP_MEM
)は、通常、適切な配置のために組み込みビルド環境のリンカースクリプトによって宣言および管理されます。
アプリケーションシナリオ
no_std
Rustは単なる学術的な演習ではなく、リソースが最優先される現実世界のアプリケーションのための強力なアプローチです。
- 組み込みシステム:おそらく最も一般的で説得力のあるユースケースです。ARM Cortex-Mシリーズ(例:IoTデバイス、ウェアラブル、産業用制御)のようなマイクロコントローラーは、キロバイト単位のRAMとフラッシュしかなく、完全なOSや
std
ライブラリには少なすぎます。no_std
Rustは、HALと組み合わせることで、開発者が低レベルで高性能な型安全なファームウェアを記述することを可能にします。 - オペレーティングシステムカーネル:RustはOS開発で注目を集めています。OSカーネルを記述するには、直接的なハードウェア相互作用、慎重なメモリ管理、そして基盤となるOSへの依存がないことが必要です。
no_std
はここで不可欠であり、開発者がRustの強力な型システムを堅牢性のために活用して、カーネルをゼロから構築することを可能にします。 - ブートローダー:システムが起動したときに実行される最初のコードであり、ハードウェアを初期化し、メインのオペレーティングシステムまたはアプリケーションをロードする責任があります。ブートローダーは非常に制約された環境で動作し、
no_std
の自然な適合先です。 - デバイスドライバー:一部のベアメタルまたは特殊なOS環境では、ドライバは完全な
std
ランタイムを関与させずに、ハードウェアと直接インターフェイスするためにno_std
Rustで記述されることがあります。 - ハイパフォーマンスコンピューティング(HPC)/科学計算(特殊ケース):あまり一般的ではありませんが、メモリレイアウトの極端な制御と、クリティカルなパフォーマンスパスでOSレベルのオーバーヘッドを避ける必要があるシナリオでは、
no_std
ライブラリまたはモジュールを、メモリと相互作用を慎重に管理していれば、より大きなstd
アプリケーションに統合できます。
結論
no_std
プログラミングにおけるRustは、開発者のための広大なフロンティアを切り開き、Rustの賞賛されている安全性、パフォーマンス、および並行性の利点を、最もリソースが制約されたベアメタル環境にまで拡張します。標準ライブラリをオプトアウトし、core
ライブラリを採用することで、開発者はコードのフットプリントと動作を細かく制御でき、Rustを組み込みシステム、オペレーティングシステムカーネル、およびバイトとサイクルごとに計算される他のニッチなアプリケーションに理想的な選択肢にします。no_std
を習得することは、単にstd
ライブラリなしでコードを書くことではありません。それは、コンピューティングの基本的なレイヤーを理解し、Rustの力を活用して、信頼性が高く効率的なシステムをゼロから構築することを意味します。