1. はじめに
この本は、Rustで組込み / ベアメタルプログラミングするための小技を集めたものです。OSにホストされた環境でプログラミングするだけでも、Rustには特有の複雑さがあります。その複雑さは、コンパイル時に型安全性、メモリ安全性、スレッド安全性を保証するための対価です。私は (そして多くのRustaceanが) 、その対価を妥当なものと考え、Rustでのプログラミングを楽しんでいます。
Rustを組込み / ベアメタル環境で使おうとすると、上述した複雑さに加えて、特別な知識が求められます。この本は、私が知る限りの特別な知識を詰め込んだクックブックです。
この本の内容は、Rust Embedded devices Working Groupが発行している組込みRustドキュメントの内容を数多く含んでいます。本書に含まれる内容の多くは、私が独自に考えたものではなく、Web上に公開されているドキュメントをテクニック集としてまとめ直したものです。WGは多くのドキュメントを公開してくれています。この本で不足する内容は、ぜひWGのドキュメントを探して見て下さい。
本書内で出典を表す際、和訳文書がある場合は、和訳文書へのリンクを示しています。これは日本人の読者を想定しているからであり、本家文書を軽視しているわけではないことを、ご了承下さい。
本書は、クックブックとして、各項目を独立して読めるようにしています。前から順番に読む、というよりは、必要や興味に応じて、辞書のようにお使い下さい。また、本書内では同じ情報がやや重複して書かれている部分もあります。これは、手抜きという一面もありますが、欲しいところに必要な情報が記載されているようにするためです。
1-1. 紙媒体で読んでいる方へ
この本は、Rust製のドキュメントビルダーmdbookで作成しています。mdbookは、htmlが一番読みやすい形式ですので、ぜひhtml形式でも読んでみて下さい。紙媒体を購入された方は、html形式の本書をダウンロードできます。参考文献へのリンクは、html形式の媒体から簡単にアクセスできます。そのため、本文内でのURL掲載は省略しています。
1-2. サンプルコードの実行
html形式で本書を読んでいる場合、掲載しているいくつかのコードは、実行して、結果を確認することができます。Rust Playpenにコードを送信して実行しますので、インターネット接続が必要です。下記コードのコードブロック右上に見えている実行ボタン (▶) をクリックすると、コンソール出力結果が確認可能です。
fn main() { println!("{}", "hello"); }
また、コードブロック右上に、時計マークのUndo changesアイコンがある場合は、コードを編集して実行することができます。上記コードのhelloを、hello worldに変更して、実行してみて下さい。
1-3. スコープ
本書には、次のトピックが含まれます。
- 組込みRust環境構築
- ベアメタル固有のプログラミングテクニック
- ツール
- ライブラリ / フレームワーク
- FFI (C言語との相互呼び出し)
- 組込みLinux
提供するシステムコマンドは、Ubuntu 18.04を想定しています。他のOSをご利用の方は、お手数ですが読み替えをお願いします。
1-4. 想定読者
下記トピックに興味があり、いずれかの項目について、ある程度開発経験がある方を想定しています。
- Rust
- 組込み / マイクロコントローラ
- 低レイヤ / ベアメタルプログラミング
1-5. 本書に含まれない内容
- なぜRustで組込みなのか?
- Rustの基礎的な文法 / イディオム
- リンカスクリプトなど低レイヤ知識の基礎事項
- クロスコンパイルの概念
- メモリマップ方式のペリフェラル制御
なぜRustで組込みなのか?は、詳しくは書きません。実行速度が速く、バイナリも小さく、安全で、生産性が高い、というのが簡単な回答です。
1-6. 参考文献
本書に含まれない内容について学習したい場合に、参考となる書籍を紹介します。
- The Rust Programming Language (TRPL)
- プログラミングRust
- 実践Rust入門
- プログラミング言語Rust 公式ガイド
- Rustの日本語ドキュメント
2. 環境構築
組込み開発では、ホストPCとは異なるアーキテクチャのバイナリを生成しなければならないため、いくつかのツールが必要になります。また、バイナリを解析したり、逆アセンブリを行ってデバッグを行う際に、便利なツールも用意しておくと良いでしょう。
ターゲットにできるアーキテクチャは多岐に渡りますし、読者が開発を行う環境もLinux / Mac / Windowsとバリエーションが多いです。これを本書で網羅することはできないため、(1) 用意するもの、(2) インストール手順を記載したWebサイトへのリンク、(3) その他備考、のみを示します。
まず、用意するもののリストは、次の通りです。
- Rust (クロスコンパイルツールチェイン含む)
- GDB
- デバッグフレームワーク (OpenOCD, JLinkなど)
- cargo-binutils
- QEMU
Cortex-Mをターゲットとするこれらのインストール手順は、The Embedded Rust Bookのインストールに記載されています。
2-1. Rust
Rustはクロスコンパイルが簡単な言語ですが、デフォルトのインストールでは、ホストマシンのネイティブコンパイルのみをサポートしています。そのため、ターゲットとするクロスコンパイラを追加するために、rustup
でターゲットを追加します。例えば、ARMのCortex-M0であれば、次の通りです。
$ rustup target add thumbv6m-none-eabi
ここで、ハマりどころがあります。
- Rustがサポートするターゲットシステムの一覧がわからない
- ターゲットシステムがサポートされていない
これらの詳細は、コンパイラサポートに記載しますが、解決方法を簡単にだけ示します。まず、ターゲットシステム一覧は、次のコマンドで取得できます。
$ rustc --print target-list
次に、ターゲットシステムがサポートされていない場合ですが、ターゲットのspecification
をJSON形式のファイルで用意します。
2-2. GDB
読者の中には、LLDBに慣れ親しんだ方も居るかと思います。通常のデバッグに関して、LLDBはGDBと同水準の機能があります。しかし、ターゲットハードウェアにプログラムをアップロードするGDBのload
コマンド相当のものが、LLDBにはありません。そのため、マイクロコントローラのファームウェア開発に限っては、GDBの利用をおすすめします。
2-3. デバッグフレームワーク
マイクロコントローラ上で動作するプログラムをGDBでデバッグするためには、SWD (Serial Wire Debug) やJTAGプロトコルを使って、GDBサーバーのサービスを提供するソフトウェアが必要になります。
このようなソフトウェアで、主要なものとしては、OpenOCDとJLinkがあります。どちらも、Rustで作成したプログラムをデバッグすることが可能です。 ターゲットとするマイクロコントローラの開発で使いやすい方を選択して下さい。Discovery環境構築では、OpenOCDの環境構築方法が記載されています。
2-4. cargo-binutils
cargo-binutilsは、LLVM binary utilitiesを簡単に利用するためのCargoサブコマンドです。llvm-objdump
やllvm-size
などをCargoから呼び出すことができます。
ターゲットアーキテクチャ用のGNU binutilsがインストールされており、そのコマンドに慣れている場合、無理に使う必要はありません。しかし、Rustでバイナリハックする上で、ターゲットアーキテクチャに依存せず、同じコマンドで利用できる、というのは大きなメリットです。
2-5. QEMU
QEMUは、有名なエミュレータです。実際のハードウェアで開発を行う前に、実験を行う場合に重宝します。本書内でも動作確認目的で、何度か利用します。
3. ベアメタルテクニック
この章では、Rustでベアメタルプログラミングするにあたり、必須 / 有用なテクニックを紹介します。この章で紹介するテクニックは、組込みだけでなく、広くベアメタルプログラミングの際に利用できます。
3-1. no_std
ベアメタルを想定したRustプログラムには、#![no_std]
アトリビュートが必須です。この#![no_std]
アトリビュートを指定すると、std
クレートではなく、core
クレートをリンクします。
std
クレートは、Rustの標準ライブラリです。例えば、皆さんが次のようなRustプログラムを書いた場合、std
クレートが使われています。
fn main() { let vector = vec!(1, 2, 3); println!("vector contains {:?}", vector); }
上記プログラムの1行目に、#![no_std]
を追加した後、▶
ボタンをクリックしてプログラムを実行してみて下さい。次のようなコンパイルエラーが発生したはずです。
error: cannot find macro `println!` in this scope
--> src/main.rs:5:5
|
5 | println!("vector contains {:?}", vector);
| ^^^^^^^
error: cannot find macro `vec!` in this scope
--> src/main.rs:4:18
|
4 | let vector = vec!(1, 2, 3);
| ^^^
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
println!
は、標準出力にフォーマットされた文字列を表示するマクロです。ベアメタル環境では、標準出力なるものは存在しません (OSが提供するものだからです) 。そのため、標準出力を利用するprintln!
マクロも利用できません。
RustのVec
は、ヒープにメモリ領域を確保します。ベアメタル環境では、ヒープメモリの確保 / 解放の機能が提供されていません。そのため、Vec
のオブジェクトを作成するvec!
マクロも、std
クレートをリンクするアプリケーションと同じようには、使えません。少し手を加えれば、ベアメタル環境でもVec
のようなコレクションを使うことが可能です。これは、メモリアロケータで解説します。
さらに、言語仕様上、パニック発生時の動作を定義する必要があります。panicの主な処理はstd
クレートで定義されています。詳しくは、panicで説明します。
core
クレートは、std
クレートのサブセットで、環境 (アーキテクチャ、OS) に依存せず使えるコードが含まれています。core
クレートは、文字列やスライスのような言語プリミティブと、アトミック操作のようなプロセッサ機能を利用するためのAPIを提供しています。
先程コンパイルエラーになったことからわかるように、core
クレートを使ったベアメタルプログラミングは、std
を利用したプログラミングとは一味違ったものになります。
出典
- Embedonomicon: [最小限の
#![no_std]
プログラム]
[最小限の#![no_std]
プログラム]: https://tomoyuki-nakabayashi.github.io/embedonomicon/smallest-no-std.html
3-2. panic
Rustのpanicは、プログラムの異常終了処理を安全に行うための機構です。例えば、下記のようなスライスの境界外アクセスは、panicを発生させます。
fn main() { let s: &[u8] = &[1, 2, 3, 4]; println!("{}", s[100]); }
上のプログラムを実行すると、下記のようなpanic発生のエラーが出力されます。
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 100', src/main.rs:3:20
C言語の未定義動作と異なり、Rustでは定義されたpanicハンドラでプログラミングエラーに対処します。OSにホストされている環境では、panicハンドラの処理が完了すると、プロセスを強制終了します。このプロセスの強制終了も、定義された動作です。
Rustのpanicについては、簡潔なQ Rustのパニック機構が詳しいです。こちらの解説にある通り、panicの主な処理は、std
クレート (std::panicに公開API、std::panicking.rsにpanic処理の本体) にあります。そのため、std
クレートをリンクしない#![no_std]
なプログラムでは、panicハンドラが未定義のままになっています。
そこで、#[panic_handler]
アトリビュートを使って、panicハンドラを定義します。最小限の#![no_std]
プログラムは、次のようになります。
#![no_main]
#![no_std]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
loop {}
}
このPanicInfo
は、panicに関する情報を提供します。Rust 1.26からは、Display
トレイトが実装されているため、フォーマットが使える環境を作ることで、panic発生時の情報を容易に得ることができます。まず、std
クレートを使い、簡単に実験できるサンプルコードをお見せします。
#![allow(unused)] fn main() { use std::panic; panic::set_hook(Box::new(|panic_info| { println!("{}", panic_info); })); panic!("Normal panic"); }
実行結果は、次のようになります。
panicked at 'Normal panic', src/main.rs:9:1
このことは、no_std
環境でも同じように使うことができます。no_std
環境でのprint!
マクロ実装方法は、print!マクロで紹介します。
use core::panic::PanicInfo;
#[panic_handler]
pub fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
assert!
マクロの失敗でも同様の情報が得られるため、非常に有用なテクニックです。
no_std
環境で利用可能なpanicハンドラを提供するクレートも存在しています。
- panic-abortは、パニックが発生すると、アボート命令を実行します。
- panic-haltは、パニックが発生すると、無限ループに入ります。
- panic-itmは、ARM Cortex-Mがターゲットの時に利用できるクレートで、パニック発生時のメッセージをITM経由でログを出力します。
- panic-semihostingは、ARM Cortex-Mがターゲットの時に利用できるクレートで、パニック発生時のメッセージを、セミホスティング機能を使ってログ出力します。
panic-abort
の実装を見ると、30行しかありません。わざわざクレートにする理由はあるのでしょうか?
panicハンドラをクレートに切り分けることで、コンパイル時のプロファイルでpanicハンドラを切り替える場合に、便利です。
// 開発プロファイル:パニックをデバッグしやすくします。`rust_begin_unwind`にブレイクポイントが置けます。
#[cfg(debug_assertions)]
extern crate panic_halt;
// リリースプロファイル:バイナリサイズを最小化します。
#[cfg(not(debug_assertions))]
extern crate panic_abort;
上記コードでは、cargo build
した時はpanic-halt
クレートと、cargo build --release
した時はpanic-abort
クレートとリンクします。
出典
- The Embedded Rust Book: 2.5.パニック
3-3. print!マクロ
ベアメタル環境でデバッグする上で、自在にテキストを表示できることは、非常に重要です。Rustでは、print!
やprintln!
マクロを使うことで、数値や文字列、構造体までフォーマットして、テキストで表示することができます。
fn main() { println!("{}, {:?}", 1, vec!(1, 2, 3)); }
マイクロコントローラでは、文字出力はUARTで行うことが多いです。例えば、今、次のような関数を使って、1文字のASCII文字を表示できるとします。土台としては、これだけあればRustの文字列フォーマッタを利用可能です。(UARTペリフェラルの初期化がが必要な点や、TXバッファに空きがあるかどうか調べなければならない点は、一旦目を瞑って下さい)
fn write_byte(c: u8) {
unsafe {
*UART0_TX = c;
}
}
この状態で数値や文字列、構造体をテキストで表示しようとすると、まず文字列に変換しなければなりません。これを、自前で実装するのは、容易ではありません。読者の中には、C言語でprintf()
関数を (部分的に) 自作した経験がある方が、多数いらっしゃるかと思います。あれはあれで貴重な経験ではありますが、Rustではより簡単に、型安全なテキスト表示マクロを実装できます。
では、上記関数を使って、std
環境と同じように利用できるprint!
/ println!
マクロを実装しましょう。
まず、全貌をお見せします。
use core::fmt::{self, Write};
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
($fmt:expr) => (print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => (print!(concat!($fmt, "\n"), $($arg)*));
}
pub fn _print(args: fmt::Arguments) {
let mut writer = UartWriter {};
writer.write_fmt(args).unwrap();
}
struct UartWriter;
impl Write for UartWriter {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.bytes() {
write_byte(c);
}
Ok(())
}
}
これで全てです。この30行にも満たないコードを追加するだけで、std
環境と同じようにprintln!
が使えます。順番に解説していきます。
まず、print!
マクロの実装です。
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::_print(format_args!($($arg)*)));
}
最も重要な部分は、format_args!
マクロの呼び出しです。format_args!
マクロは、コンパイラ組込みの手続きマクロで、文字列フォーマットの中心を担うAPIです。このマクロは、与えられたフォーマット文字列と引数群から、core::fmt::Arguments
を構築するコードを生成します。この辺りの話については、Rustの文字列フォーマット回り (改訂版)で非常に詳しく解説されています。ここでは詳細を割愛します。
$crate::_print()
は、format_args!
マクロの出力であるcore::fmt::Arguments
を引数に取るラッパー関数です。
pub fn _print(args: fmt::Arguments) {
let mut writer = UartWriter {};
writer.write_fmt(args).unwrap();
}
フォーマット文字列をUARTに出力するUartWriter
構造体のオブジェクトを作成し、core::fmt::Write
トレイトのwrite_fmt
メソッドを呼び出します。UartWriter
は、ここではかなり実装を簡略化しており、中身のない空の構造体です。ハードウェアの排他制御などは、今回は考慮に入れていません。
struct UartWriter;
impl Write for UartWriter {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.bytes() {
write_byte(c);
}
Ok(())
}
}
フォーマット文字列を取り扱うために、UartWriter
はcore::fmt::Write
トレイトを実装します。write_fmt
メソッドは、デフォルトメソッドなので、write_str
だけ実装すれば良いです。write_str
メソッドの関数シグネチャは、fn (&mut self, &str) -> fmt::Result
となっており、&str
の形で渡されるフォーマット済み文字列をどのように出力するか、を実装します。上記コードでは、イテレータで1バイトずつ取得し、write_byte
関数でUARTに1バイトずつ送信します。
それでは、実行してみましょう。03-bare-metal/print
ディレクトリに、QEMUで動作するサンプルがあります。リセットベクタ内で、println!
マクロを呼び出します。
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
println!("Hello {}", "Rust");
// 中略
}
次のコマンドで実行できます (thumbv7m-none-eabi
のクロスコンパイラとqemu-system-armが必要です) 。
$ cargo run
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/print`
Hello Rust
注意:このサンプルはQEMUでしか動作しません。QEMUのUARTは初期設定不要で雑に使えるため、非常に便利です。
また、panicで紹介した通り、panic時の情報を表示する際も便利です。
pub unsafe extern "C" fn Reset() -> ! {
panic!("explicit panic!");
}
#[panic_handler]
fn panic(panic: &PanicInfo) -> ! {
println!("{}", panic);
loop {}
}
実行すると、panicを発生させたソースコードの位置と、メッセージを表示します。
panicked at 'explicit panic!', src/main.rs:10:5
出典
- Discovery: urpintln!
3-4. リンカ
ベアメタルプログラミングを行う上で、リンカは避けられない要素です。ここでは、Rustでシンボルやセクションを扱う方法について、説明します。
シンボル名とセクション配置
C++と同様に、デフォルトではRustのシンボルは、コンパイラによってマングルされます。コンパイラが生成したシンボル名は、コンパイラのバージョンごとに異なる可能性があります。そこで、次のアトリビュートを使用して、シンボル名やセクション配置を制御します。
#[export_name = "foo"]
は、関数や変数のシンボル名をfoo
に設定します。#[no_mangle]
は、関数名や変数名をマングルせず、そのままシンボル名として使用します。#[link_section = ".bar"]
は、対象のシンボルを、.bar
というセクションに配置します。
#[no_mangle]
は、基本的に、#[export_name = <item-name>]
のシンタックスシュガーです。このことから、#[no_mangle]
と[#link_section]
との組み合わせで、任意のシンボルを特定のセクションに配置できます。
次のコードは、ARM Cortex-Mシリーズのリセットベクタを指定セクションに配置する例です。Reset
関数の関数ポインタを、RESET_VECTOR
というシンボルで、.vector_table.reset_vector
セクションに配置します。もちろん、このセクションはリンカスクリプトで定義されている必要があります。
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;
補足:上のコードで
extern
を使用している理由は、Cortex-MシリーズのハードウェアがリセットハンドラにC ABIを要求するためです。
リンカスクリプトのシンボルを参照
Rustからリンカスクリプトのシンボルを参照することも可能です。これは、.bss
セクションのゼロクリアや、.data
セクションの初期化に利用できます。
次のように、リンカスクリプトでセクションの開始位置と終了位置にシンボルを作成します。
SECTIONS
{
.bss :
{
_sbss = .;
*(.bss .bss.*);
_ebss = .;
} > RAM
.data : AT(ADDR(.rodata) + SIZEOF(.rodata))
{
_sdata = .;
*(.data .data.*);
_edata = .;
} > RAM
_sidata = LOADADDR(.data);
これらのリンカスクリプトで作成したシンボルは、Rustで次のように利用できます。これは、.bss
セクションのゼロクリアと、.data
セクションの初期化を行うコードの例です。
use core::ptr;
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
// RAMの初期化
extern "C" {
static mut _sbss: u8;
static mut _ebss: u8;
static mut _sdata: u8;
static mut _edata: u8;
static _sidata: u8;
}
let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize;
ptr::write_bytes(&mut _sbss as *mut u8, 0, count);
let count = &_edata as *const u8 as usize - &_sdata as *const u8 as usize;
ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, count);
loop {}
}
リンカスクリプトで作成したシンボルをu8
の変数として、そのアドレスを利用します。
extern
externは、Rustのキーワードで、外部とのインタフェースに使用されます。外部クレートとの接続にも使われますが、組込みでの重要な利用方法はFFI (Foreign function interfaces) です。
他言語の変数や関数を利用する場合、下記の通りextern
ブロック内でインタフェースを宣言します。
extern "C" {
static mut _sbss: u8;
// ...
}
逆に、他言語からRustのコードを呼ぶ場合は、次のように関数シグネチャを宣言します。
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
// ...
}
FFIだけでなく、どこか外部にあるRustコードを宣言することも可能です。
extern "Rust" {
fn main() -> !;
}
コラム〜RustのABIは安定化していない!?〜
意外に思うかもしれませんが、RustのABIは定義されていません。4年前からこのissueで議論が続けられています。
そのため、Rustで安定したABIを提供するためには、extern "C"
を用いてC言語のABIを使用しなければなりません。
/// C ABI
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
/* ... */
}
linkageアトリビュート
linkage
アトリビュートは、まだunstableの状態です。linkage featureのissueで議論が続いています。このアトリビュートは、シンボルのリンケージを制御するものです。例えば、特定のシンボルをweakにしてデフォルト実装を与えたり、明示的に外部リンケージにすることができます。
出典
- Embedonomicon
3-5. アセンブリ
Rustでアセンブリを使う方法は2つあります。インラインアセンブリ(asm!) と 自由形式アセンブリ(global_asm!) です。本当に困ったことに、両方共stableでは使えません。
ベアメタルプログラミングをする上で、stableで機能が不足することは往々にしてあることです。ここでは、stableで頑張る方法と、nightlyと共に歩む道、両方を紹介します。
stableでのアセンブリ
stableでアセンブリを書く方法は、外部ファイルに書くことです。.s
ファイルにアセンブリを書いておき、アセンブラを使ってオブジェクトファイル (.o
) にアセンブルし、アーカイブ (.a
) を作り、Rustのコードとリンクします。
この方法では、ターゲットアーキテクチャのアセンブラが必要です。例えば、ARM Cortex-Mをターゲットにする時、アセンブラとして、arm-none-eabi-gcc
(arm-none-eabi-as
) を使います。
Makefile
を使うこともできますが、よりRustらしく、ビルドスクリプト
を作成します。ccクレートを利用し、ビルドスクリプト内でC言語 (C++やアセンブリも可) のコードをビルドします。
今、Cargoプロジェクトのトップディレクトリにアセンブリを書いたasm.s
があるとします。ビルドスクリプトは、次のようになります。
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};
use cc::Build;
fn main() -> Result<(), Box<Error>> {
// このクレートのビルドディレクトリです
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
// ライブラリサーチパスを追加します
println!("cargo:rustc-link-search={}", out_dir.display());
// `link.x`をビルドディレクトリに置きます
File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;
// `asm.s`ファイルをアセンブルします
Build::new().file("asm.s").compile("asm");
Ok(())
}
Cargo.toml
に依存関係を追加します。
[build-dependencies]
cc = "1.0.36"
これだけで、外部アセンブリファイルをアセンブルし、アーカイブファイルを作り、Rustコードとリンクしてくれます。
豆知識〜ccクレートでコンパイラを指定〜
cc
クレートで任意のコンパイラを使用したい場合、環境変数での設定が可能です。例えば、次のようにコマンドを実行します。
CC=/opt/toolchain/arm-none-eabi-gcc cargo build
CFLAGS
環境変数によるコンパイラフラグの指定も可能です。
豆知識〜ビルド生成物の配布〜
クレートと共に、あらかじめビルドした生成物を配布することができます。cc
クレートを使う場合、ビルドマシンにターゲットアーキテクチャのアセンブラが必要です。クレートのユーザーのマシンに、このアセンブラがなくても、クレートを使ってもらえます。詳しいやり方は、Embedonomiconのstableでのアセンブリを参照して下さい。
nightlyでのアセンブリ
asm!
まずは、インラインアセンブリのasm!
マクロです。1命令のアセンブリを書く時に便利です。
#![feature(asm)]
pub unsafe fn wfi() {
asm!(wfi :::: "volatile");
}
記法の詳細は、インラインアセンブリに記載しています。x86アセンブリは、デフォルトではAT&T記法ですが、オプションによりintel記法で書くことも可能です。
マクロでラッパ関数を生成すると便利です。
macro_rules! instruction {
($fnname:ident, $asm:expr) => (
#[inline]
pub unsafe fn $fnname() {
match () {
#[cfg(target_arch = "thumv7m")]
() => asm!($asm :::: "volatile"),
}
}
)
}
// wfi(), wfe()として利用可能です
instruction!(wfi, "wfi");
instruction!(wfe, "wfe");
// ...
global_asm!
まとまったアセンブリを書く時に便利です。
#![feature(global_asm)]
#[cfg(target_arch = "thumv7m")]
#[link_section = ".text.boot"]
global_asm!(r#"
halt:
wfe
b halt
"#);
外部アセンブリをインクルードすることもできます。
$ cat asm.s
.section ".text.boot"
.global halt
wfe
b halt
#![feature(global_asm)]
global_asm!(include_str!("asm.s"));
出典
- Embedonomicon: stableでのアセンブリ
3-6. メモリアロケータ
ベアメタルプログラミングでも開発が進むと動的なコレクションが使いたくなります。std
が使える通常のRustでは、Vec
やString
といった一般的なコレクションが利用できます。このようなコレクションは、ヒープメモリを利用します。そのため、デフォルトでは、no_std
な環境では、これらのコレクションを利用できません。しかし、no_std
なRustでも、メモリアロケータを実装することで、コレクションを利用することができます。
メモリアロケータを実装せずにコレクションを利用する方法は、heaplessで説明します。
ただ、(執筆時点のRust 1.35.0では) 残念なことにnightly必須です。ベアメタルでメモリアロケータを実装するには、allocとalloc_error_handlerのフィーチャが必要です。alloc
は、Rust 1.36でstableになるため、本書が世に出回っている時点では、stableになっています。一方、alloc_error_handler
については、まだ安定化の目途が立っていないようです。今しばらく、メモリアロケータの実装はnightly専用になりそうです。
一時的にツールチェインをnightlyに切り替えます。Cortex-M3を例に解説します。nightlyのツールチェインにCortex-M3用のターゲットを追加します。
$ rustup override set nightly
$ rustup target add thumbv7m-none-eabi
ここでの目標は、次のプログラムを動作させることです。
pub unsafe extern "C" fn Reset() -> ! {
let mut xs = Vec::new();
xs.push(42);
xs.push(83);
println!("{:?}", xs);
}
println!
マクロの実装方法については、print!マクロで説明しています。
グローバルアロケータ
Vec
やString
といったコレクションは、デフォルトではグローバルアロケータを使ってヒープメモリ領域を確保します。グローバルアロケータとは、#[global_allocator]
アトリビュートが指定されたアロケータのことです。このアトリビュートで指定するオブジェクトは、GlobalAllocトレイトを実装しなければなりません。
// グローバルメモリアロケータの宣言
// ユーザはメモリ領域の`[0x2000_0100, 0x2000_0200]`がプログラムの他の部分で使用されないことを
// 保証しなければなりません
#[global_allocator]
static HEAP: BumpPointerAlloc = BumpPointerAlloc {
head: UnsafeCell::new(0x2000_0100),
end: 0x2000_0200,
};
それでは、グローバルアロケータに指定するBumpPointerAlloc
の実装を見てみましょう。
BumpPointerAlloc
これから、BumpPointerAlloc
という最も単純なアロケータを実装します。このアロケータは、次のようにヒープメモリを管理します。
- 初期化時に、ヒープメモリ領域の開始アドレスと終了アドレスを受け取ります
- 割り当て可能なメモリ領域の先頭ポインタを1つだけ保持します
- メモリを新しく割り当てると、割り当てた分だけ単純に先頭ポインタを増加します
- 一度割り当てたメモリは、解放しません
上述した通り、このアロケータは、GlobalAlloc
トレイトを実装します。全体を示します。
use core::ptr;
use core::cell::UnsafeCell;
use core::alloc::GlobalAlloc;
extern crate alloc;
use alloc::alloc::Layout;
use alloc::vec::Vec;
// *シングル*コアシステム用のポインタを増加するだけのアロケータ
struct BumpPointerAlloc {
head: UnsafeCell<usize>,
end: usize,
}
unsafe impl Sync for BumpPointerAlloc {}
unsafe impl GlobalAlloc for BumpPointerAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let head = self.head.get();
let align = layout.align();
let res = *head % align;
let start = if res == 0 { *head } else { *head + align - res };
if start + align > self.end {
// ヌルポインタはメモリ不足の状態を知らせます
ptr::null_mut()
} else {
*head = start + align;
start as *mut u8
}
}
unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
// このアロケータはメモリを解放しません
}
}
順番に解説します。
struct BumpPointerAlloc {
head: UnsafeCell<usize>,
end: usize,
}
まず、このアロケータは、割り当て可能なメモリ領域の先頭を示すhead
と、末尾を示すend
を持ちます。head
がUnsafeCell
になっている理由は、&self
を引数に取るalloc
メソッドの中でhead
の値を書き換えるためです。alloc
メソッドのシグネチャは、GlobalAlloc
トレイトで定義されているため、引数を&mut self
に変更することができません。
unsafe impl Sync for BumpPointerAlloc {}
次にSync
トレイトを実装します。これは、グローバルアロケータのオブジェクトがstatic
変数になるため、スレッド間で安全に共有できることをコンパイラに伝えるためです。
GlobalAlloc
トレイトの実装で求められるメソッドは、alloc
とdealloc
のみです。dealloc
はメモリを解放しないため、何もしません。
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let head = self.head.get();
let align = layout.align();
let res = *head % align;
let start = if res == 0 { *head } else { *head + align - res };
if start + align > self.end {
// ヌルポインタはメモリ不足の状態を知らせます
ptr::null_mut()
} else {
*head = start + align;
start as *mut u8
}
}
引数layout
(Layout
) は、要求されているメモリブロックです。align()
で、アライメントを考慮して、確保しなければならないメモリブロックサイズを返します。head
のアドレスがend
に到達するまで、単純にポインタを増加しながら、メモリを割り当てます。
なお、この実装は、割り込みでメモリアロケータを使用する場合、データ競合が発生し、安全に利用できません。
alloc_error_handler
最後の要素が、アロケーションエラー発生時のハンドラです。これは、#[alloc_error_handler]
アトリビュートを指定します。
#[alloc_error_handler]
fn on_oom(_layout: Layout) -> ! {
loop {}
}
今回は、単純に無限ループに陥るだけの実装です。
動作確認
03-bare-metal/allocator
ディレクトリに、Cortex-M3をターゲットにした場合のサンプルコードがあります。ディレクトリに移動し、次のコマンドで実行結果が確認できます。
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.15s
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/allocator`
[42, 83]
無事、Vec
のデバッグ表示が確認できました。
メモリアロケータ実装例
ここで紹介したBumpPointerAlloc
は実用に耐えないものです。いくつか、より洗練されたメモリアロケータの実装例を紹介します。
linked-list-allocator
linked-list-allocatorは、BlogOSの著者が公開しているlinked-listを使ったアロケータです。Writing an OS in Rust (First Edition) Kernel Heapに少し解説があります。
Redox Slab allocator
Redox Slab allocatorは、RustでOSを作るプロジェクト「Redox」のメモリアロケータです。僭越ながら、簡単な解説をRedox Slab Allocatorで学ぶRustベアメタル環境のヒープアロケータに書いています。
alloc-cortex-m
alloc-cortex-mは、linked-list-allocatorを、Cortex-MのMutexを使ってラッピングしたメモリアロケータです。
kernel-roulette
kernel-rouletteは、RustでLinux kernelのdriverを書くプロジェクトです。このプロジェクトでは、kmalloc
やkfree
をFFIで呼び出し、Linux kernelの機能を用いてRustのメモリアロケータを実装します。
出典
- The Embedded Rust Book: コレクション
3-7. entryポイント
ベアメタルプログラミングの開始地点では、シンボルを駆使したプログラミンを行います。このことは時として、Rustの安全性に頼らず、開発者が安全性を保証しなければならないことを意味します。
ベアメタルプログラムの最初のプロセスは、無限ループを実行し、プロセスが決して停止しないように実装します。ここでは、Rustの型検査を用いて、ベアメタルプログラミングでのエントリーポイントを再利用性が高く、安全にする方法について説明します。
再利用できるリセットハンドラ
一度作って終わり、であればリセットハンドラを再利用可能とすることに、あまり意味はありません。私個人としては、自身の作るプログラムをより良い設計のものにしたり、他人に使ってもらいたいです。また、将来的に、自分が新しくプログラムを作ったり、既存のものを作り直す場合に、ソフトウェアの再利用性が高いことが重要です。
そこで、まず、リセットハンドラをアプリケーション (2nd stageブートローダかもしれませんし、OSかもしれません) から独立させます。
#![no_std]
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
extern "Rust" {
fn main() -> !;
}
main()
}
上のプログラムでは、リセットハンドラは、「外部にあるRust ABIのmain
というシンボルがついた関数」を呼び出します。このリセットハンドラをクレートとして切り出せば、リセットハンドラとアプリケーションとを、異なるクレートで管理できます。リセットハンドラのクレートをreset
クレートとします。アプリケーションクレートでは、次のようにmain
関数を定義します。
#![no_std]
#![no_main]
extern crate reset;
#[no_mangle]
pub fn main() -> ! {
let _x = 42;
loop {}
}
ただし、このシンボルをインタフェースとしてやり方は、安全ではありません。アプリケーション側のmain
関数を!
なしで定義しても、コンパイルが通ってしまい、未定義動作を引き起こす可能性があります。
型安全にする
クレートの利用者や、将来の自分 (!) が誤った使い方をできないようにしましょう。シンボルの代わりに、マクロをインタフェースとして利用します。
#[macro_export]
macro_rules! entry {
($path:path) => {
#[export_name = "main"]
pub unsafe fn __main() -> ! {
// 与えられたパスの型チェック
let f: fn() -> ! = $path;
f()
}
}
}
アプリケーションは、次のようにこのマクロを利用します。
#![no_std]
#![no_main]
use rt::entry;
entry!(main);
fn main() -> ! {
let _x = 42;
loop {}
}
マクロ内で型チェックを行うため、main
の戻り値が!
でなければ、コンパイルエラーになります。
補足:発散する関数
Rustの関数には、発散する関数 (diverging functions) という種別があります。これは、The Book 1st edition: Functions#diverging-functionsに説明があります。
戻り値に!
の型を持つ関数は、決してその関数から戻らないことを意味します。次のプログラムをビルド (実行) してみて下さい。
fn main() -> ! { println!("I'll be back!"); }
次のようなエラーが発生したはずです。
error[E0308]: mismatched types
--> src/main.rs:1:14
|
1 | fn main() -> ! {
| ---- ^ expected !, found ()
| |
| this function's body doesn't return
|
= note: expected type `!`
found type `()`
下記エラーが示す通り、!
を戻り値として持つ関数が、関数から戻るようなコードになっている場合には、コンパイルエラーになります。
this function's body doesn't return
次に、無限ループを挿入して、再び実行してみます。
fn main() -> ! { println!("I never return!!"); loop {} }
Rust Playgroundでは、タイムアウトでプロセスが強制終了されますが、コンパイルが通ることがわかります。
出典
- Embedonomicon: mainインタフェース
4. ツール
Rustで組込み / ベアメタルプログラミングする上で欠かせないツールを紹介します。Embedded WG tool teamにも情報があるので、チェックしてみて下さい。
3-1. Cargo
Rustでの開発にCargoは欠かせません。Cargoは、Rustのパッケージマネージャですが、それ以上のことができます。3rd party製のサブコマンド拡張をインストールすることで、Cargoの機能を拡張できます。ここでは、組込み / ベアメタルでのRust開発をより便利にするCargoに機能やサブコマンド拡張について紹介します。
設定ファイル
まず、欠かせないのが、設定ファイルです。どのような設定項目が書けるか、はCargo: 3.3 Configurationに掲載されています。
Cargo設定ファイルはTOML形式で記述し、プロジェクトの.cargo/config
に作成することが多いです。実際は、階層的な作りになっています。どのような階層構造になっているか、はCargo: 3.3 Configuration 階層構造を参照して下さい。
組込み / ベアメタルでよく使う設定項目は、target
とbuild
です。
target.$triple
target.$triple
はターゲットトリプルごとに、カスタムする内容を設定します。$triple
の部分に、有効なターゲットトリプルを指定します。カスタムランナーの設定やコンパイルオプションの指定などに使います。例えば、Cortex-M3ターゲットの時はqemu-system-arm
で、RISC-Vターゲットの時はqmue-system-riscv32
を、それぞれカスタムランナーにしたい場合、次のように設定ファイルを記述します。
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -machine lm3s6965evb -nographic -kernel"
[target.riscv32imac-unknown-none-elf]
runner = "qemu-system-riscv32 -nographic -machine sifive_u -kernel"
これで、cargo run --target thumbv7m-none-eabi
やcargo run --target riscv32imac-unknown-none-elf
というコマンドを実行すると、QEMUでビルドしたバイナリを実行します。
次の4項目が設定できます。
- linker = ".."
- ar = ".."
- runner = ".."
- rustflags = ["..", ".."]
target.'cfg
target.$triple
は、ターゲットトリプルを完全に指定する方法です。一方、target.'cfg
は、条件を複数指定して、カスタマイズできます。
下記の例は、ターゲットアーキテクチャが32bitのARMで、OSなしのターゲットトリプル全てに適用されます。
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
"-C", "link-arg=-Tlink.x",
]
build
デフォルトのターゲットシステムが固定の場合、cargo run --target thumbv7m-none-eabi
という長いコマンドを毎回入力するのは面倒です。そこで、build
設定でデフォルトターゲットシステムを指定できます。
[build]
target = "thumbv7m-none-eabi"
cargo run
で、cargo run --target thumbv7m-none-eabi
と等価になります。
binutils
組込み / ベアメタルの開発において、バイナリを調査することは、息をするより自然なことです。バイナリの調査を行う際、objdump
、size
、readelf
、nm
などのツールを利用します。GNUのbinutilsを使用しても良いのですが、LLVMのものを利用すると、rustcがサポートするターゲットアーキテクチャ全てに対応しており、便利です。
Cargoのサブコマンドであるcargo binutilsが提供されており、Cargoからobjdump
やsize
コマンドを利用できます。
インストールも非常に簡単です。
$ cargo install cargo-binutils
$ rustup component add llvm-tools-preview
プロジェクトバイナリ名がapp
の場合、次のように使用します。
$ cargo size --target thumbv7m-none-eabi --bin app
Cargo設定ファイルで、buildターゲットを指定している場合、次のコマンドで同じことができます。
$ cargo size --bin app
リリースビルドしたバイナリを調査する場合、--release
を追加します。
$ cargo size --bin app --release
LLVMツール自体のオプションを使用する場合、空の--
を入力した後にLLVMのオプションを指定します。
$ cargo objdump --bin app -- -d -no-show-raw-insn
# ^^^^^^^^^^^^^^^^^^^^^^^
3-2. コンパイラサポート
Rustコンパイラがサポートしているターゲットについてまとめます。また、ターゲットとするシステムをRustがサポートしていない場合、どのような対応が考えられるか、についても記載します。
Rustプラットフォームサポート
Rustがサポートしているプラットフォーム一覧は、Rust Platform Supportに記載されています。
ここでは、ターゲットシステムを、Tier1からTier3に分類しています。Tier1は「ビルドでき、かつ、動作することが保証されている」ものです。Tier2は「ビルドできることが保証されている」ものです。Tier3は「サポートされているが、ビルドできる保証がない」ものです。
あるアーキテクチャに対して、Rustが提供する最低レベルのサポートは、有効化されているLLVMバックエンドがあることです。次のコマンドにより、Rustコンパイラが使用するLLVMでサポートが有効になっているアーキテクチャを確認できます。
$ cargo objdump -- -version
LLVM (http://llvm.org/):
LLVM version 8.0.0-rust-1.34.1-stable
// 途中略
Registered Targets:
aarch64 - AArch64 (little endian)
aarch64_be - AArch64 (big endian)
arm - ARM
arm64 - ARM64 (little endian)
armeb - ARM (big endian)
hexagon - Hexagon
mips - MIPS (32-bit big endian)
mips64 - MIPS (64-bit big endian)
mips64el - MIPS (64-bit little endian)
mipsel - MIPS (32-bit little endian)
msp430 - MSP430 [experimental]
nvptx - NVIDIA PTX 32-bit
nvptx64 - NVIDIA PTX 64-bit
ppc32 - PowerPC 32
ppc64 - PowerPC 64
ppc64le - PowerPC 64 LE
riscv32 - 32-bit RISC-V
riscv64 - 64-bit RISC-V
sparc - Sparc
sparcel - Sparc LE
sparcv9 - Sparc V9
systemz - SystemZ
thumb - Thumb
thumbeb - Thumb (big endian)
wasm32 - WebAssembly 32-bit
wasm64 - WebAssembly 64-bit
x86 - 32-bit X86: Pentium-Pro and above
x86-64 - 64-bit X86: EM64T and AMD64
もし、手元の環境にLLVMツールの最新版がインストールされている場合、そのターゲットアーキテクチャと見比べて見て下さい。執筆時点での著者の環境では、LLVM 8.0.1がリリースされており、ターゲットアーキテクチャは以下の通りでした。
$ llvm-objdump -version
LLVM (http://llvm.org/):
LLVM version 8.0.1
// 中略
Registered Targets:
aarch64 - AArch64 (little endian)
aarch64_be - AArch64 (big endian)
amdgcn - AMD GCN GPUs
arm - ARM
arm64 - ARM64 (little endian)
armeb - ARM (big endian)
avr - Atmel AVR Microcontroller
// 中略
x86 - 32-bit X86: Pentium-Pro and above
x86-64 - 64-bit X86: EM64T and AMD64
xcore - XCore
amdgcn
、avr
、xcore
など、Rustコンパイラではサポートされていないアーキテクチャがあります。Rustコンパイラではこれらのアーキテクチャサポートが無効化されて、配布されています。
Rustがサポートしていないターゲットのビルド
ここから先は、著者が試したことがないため、参考情報となります。
もし使用したいターゲットが、Rustコンパイラで無効化されている場合 (上述のamdcgn
やavr
) 、Rustのソースコードを修正しなければなりません。rust-lang/rust#52787の最初の2つのコミットがヒントになります。
メインラインのLLVMがターゲットアーキテクチャをサポートしていない場合でも、LLVMのforkが存在しているのであれば、rustc
のビルド前にLLVMを差し替えることが可能です。Rust on the ESP and how to get startedでは、LLVMのXtensa forkを使用し、ESPをターゲットにRustのコードをコンパイルする方法が紹介されています。
もしGCCでしかターゲットがサポートされていない場合、mrustcを使うことができます。これは、非公式のRustコンパイラで、RustプログラムをCコードに変換し、その後、GCCを使ってコンパイルします。
target specification
Rustでは、ターゲットシステムに関連するターゲット仕様があります。この仕様では、アーキテクチャ、オペレーティングシステム、データレイアウトなどを記述します。
Rustコンパイラに組み込まれているターゲット仕様があります。コンパイラ組込みのターゲットは、次のコマンドで確認できます。
$ rustc --print target-list | column
aarch64-fuchsia mips64el-unknown-linux-gnuabi64
aarch64-linux-android mipsel-unknown-linux-gnu
aarch64-unknown-cloudabi mipsel-unknown-linux-musl
aarch64-unknown-freebsd mipsel-unknown-linux-uclibc
aarch64-unknown-linux-gnu msp430-none-elf
//中略
i686-unknown-openbsd x86_64-unknown-linux-gnux32
mips-unknown-linux-gnu x86_64-unknown-linux-musl
mips-unknown-linux-musl x86_64-unknown-netbsd
mips-unknown-linux-uclibc x86_64-unknown-openbsd
mips64-unknown-linux-gnuabi64 x86_64-unknown-redox
次のコマンドを使って、ターゲット仕様を表示できます (nightlyコンパイラが必要です)。
$ rustc +nightly -Z unstable-options --print target-spec-json --target thumbv7m-none-eabi
{
"abi-blacklist": [
"stdcall",
"fastcall",
"vectorcall",
"thiscall",
"win64",
"sysv64"
],
"arch": "arm",
"data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
"emit-debug-gdb-scripts": false,
"env": "",
"executables": true,
"is-builtin": true,
"linker": "rust-lld",
"linker-flavor": "ld.lld",
"llvm-target": "thumbv7m-none-eabi",
"max-atomic-width": 32,
"os": "none",
"panic-strategy": "abort",
"relocation-model": "static",
"target-c-int-width": "32",
"target-endian": "little",
"target-pointer-width": "32",
"vendor": ""
}
もし、ターゲットとするシステムに対して、コンパイラ組込みのターゲットがない場合、JSON形式でカスタムターゲット仕様を作成します。ターゲットとするシステムに近いコンパイラ組込みターゲットを、上記コマンドで表示し、それをカスタムする方法がお勧めです。本書の執筆時点では、ターゲット仕様の各フィールドが何を意味するか説明する最新のドキュメントがありません。ターゲット仕様時点から、追加、変更されているものについては、コンパイラのソースコードを確認する必要があります。
ターゲット仕様ファイルを用意した後は、ファイルパスで指定するか、その名前で参照できます。
$ cargo build --target custom.json
# もしくは
$ cargo build --target custom
出典
- Embedonomicon: コンパイラサポートに関する覚書
3-3. rustc
rustcは、Rustのコンパイラです。組込み / ベアメタルプログラミングに限りませんが、コンパイラでできることを知っていると便利なことがあります。
コマンドライン引数
コマンドライン引数を直接rustcに指定する機会は少ないです。多くの場合、Cargoの設定を記述し、間接的にrustcのコマンドライン引数を使用します。しかし、rustcのコマンドライン引数を把握していなければ、Cargoから使いようもありません。網羅的な説明は、コマンドライン引数を参照して下さい。
組込み / ベアメタルプログラミングで最も大事なコマンドライン引数は、間違いなく-C / --codegen
です。このコマンドライン引数では、使用するリンカの指定や最適化レベルなどの制御ができます。詳しくは、コード生成オプションに記載します。
組込みLinux開発でターゲットシステムのライブラリに依存するクレートをクロスビルドするのであれば、--sysroot
オプションでsystem rootのパスを上書きすることができます。
コンパイラより厳密なソースコード検査をするために、コマンドライン引数からlintルールごとに、lintレベルを制御できます。-A
, -W
, -D
, -F
フラグがあり、それぞれ、許可、警告、拒絶、禁止を意味します。lintルールごとに、これらのフラグを設定できます。詳細は、lintで紹介します。下に例を示します。
$ rustc lib.rs --crate-type=lib -W missing-docs
lint
lint
はソースコードをコンパイラより厳密なルールに則り、検査するためのツールです。Rustコンパイラには、様々なlintルールが組み込まれています。ソースコードをコンパイルする時、自動的にlintによる検査が行われます。
プロジェクトの運用ルールに合わせて、適切なlintルールを設定することで、ソースコードの品質をより向上できるでしょう。
lintレベル
rustcのlintレベルは、4つに分類されます。
- allow (許可)
- warn (警告)
- deny (拒絶)
- forbid (禁止)
各lintルールには、デフォルトのlintレベルがあり、コンパイルオプションかアトリビュートで上書きできるようになっています。まず、lintレベルについて説明します。
allow (許可)
lintルールを適用しません。例えば、次のコードをコンパイルしても、警告は発生しません。
pub fn foo() {}
$ rustc lib.rs --crate-type=lib
しかし、このコードはmissing_docs
ルールを違反しています。lintレベルを上書きしてコンパイルすると、コンパイルエラーになったり、警告が出力されるようになります。
warn (警告)
lintルール違反があった場合、警告を表示します。
fn main() { let x = 5; }
このコードはunused_variables
のルールに違反しており、次の警告が報告されます。
warning: unused variable: `x`
--> src/main.rs:2:9
|
2 | let x = 5;
| ^ help: consider prefixing with an underscore: `_x`
|
= note: #[warn(unused_variables)] on by default
deny (拒絶)
lintルール違反があった場合、コンパイルエラーになります。
fn main() { 100u8 << 10; }
このコードは、exceeding_bitshifts
ルールに違反しており、コンパイルエラーになります。
error: attempt to shift left with overflow
--> src/main.rs:2:5
|
2 | 100u8 << 10;
| ^^^^^^^^^^^
|
= note: #[deny(exceeding_bitshifts)] on by default
forbid (禁止)
lintルール違反があった場合、コンパイルエラーになります。forbid
は、deny
より強いレベルで、上書きができません。
下のコードは、アトリビュートでmissing_docs
ルールをallowに上書きしています。
#![allow(missing_docs)]
pub fn foo() {}
missing_dogs
ルールを、denyレベルに設定してコンパイルすると、このコードはコンパイルできます。
$ rustc lib.rs --crate-type=lib -D missing-docs
一方、forbidレベルに設定してコンパイルすると、コンパイルエラーになります。
$ rustc lib.rs --crate-type=lib -F missing-docs
error[E0453]: allow(missing_docs) overruled by outer forbid(missing_docs)
--> lib.rs:1:10
|
1 | #![allow(missing_docs)]
| ^^^^^^^^^^^^ overruled by previous forbid
|
= note: `forbid` lint level was set on command line
lintレベルの設定方法
コンパイラフラグで設定
コンパイルオプションで、-A
, -W
, -D
, -F
のいずれかを指定して、lintレベルを設定できます。
$ rustc lib.rs --crate-type=lib -W missing-docs
もちろん、複数のフラグを同時に設定することも可能です。
$ rustc lib.rs --crate-type=lib -D missing-docs -A unused-variables
Cargoの設定ファイル内で、lintレベルを設定することも可能です。
$ cat .cargo/config
[build]
rustflags = ["-D", "unsafe-code"]
アトリビュートで設定
ソースコード内のアトリビュートで、allow
, warn
, deny
, forbid
のいずれかを指定して、lintレベルを設定できます。
$ cat lib.rs
#![warn(missing_docs)]
pub fn foo() {}
1つのアトリビュートに、複数のlintルールを指定できます。
#![warn(missing_docs, unused_variables)]
fn main() {
pub fn foo() {}
}
複数のアトリビュートを組み合わせて使うこともできます。
#![warn(missing_docs)]
#![deny(unused_variables)]
pub fn foo() {}
lintルール
次のコマンドでlintルールと、デフォルトレベルの一覧が取得できます。
$ rustc -W help
デフォルトレベルごとに、サンプルコード付きでlintルールが説明されています。
コラム〜Rustのlintツールclippy〜
さらに細かなlintルールで検査したい場合、clippyが使用できます。clippyは、下記のようなルールを含んでいます。
- 不必要にコードを複雑にする書き方の検出
- 正当性がないコードの検出 (常に条件が真になるなど)
- 性能が低下するコードの検出
導入も容易なため、プロジェクトの初期段階からclippyを導入することをお勧めします。clippy lintルール一覧も合わせてご覧ください。
コード生成オプション
Codegen optionsにコード生成に関するオプション一覧がまとめられています。
組込みで特に重要な最適化オプションについて説明します。デフォルト (cargo build) では、最適化を行いません。コンパイラオプションとしては、-C opt-level = 0
を使用します。
速度最適化
rustc
は、3つの最適化レベルを提供しています。opt-level = 1
, 2
, 3
です。cargo build --release
を実行した場合、デフォルトでは、opt-level = 3
です。
opt-level = 2
, 3
では、バイナリサイズを犠牲にする (大きくする) ことで、速度を向上します。例えば、opt-level = 2
以上では、ループ展開が行われます。ループ展開は、Flash/ROMの容量をより多く使用します。
組込みでは、速度よりもバイナリサイズが制限になる場合があります。その場合には、バイナリサイズの最適化が必要です。
サイズ最適化
rustc
は、2つのサイズ最適化レベルを提供しています。opt-level = "s"
, "z"
です。"z"
は、"s"
より小さなバイナリを作ります。
これらの最適化レベルは、LLVMのインライン展開しきい値を下げます。インライン展開しきい値は、-C inline-threshold
で指定することもできます。Rust 1.34.1でのしきい値の使われ方は、ソースコードを見るとわかります。
opt-level = 3
は275opt-level = 2
は225opt-level = "s"
は75opt-level = "s"
は25
出典
5. ライブラリ / フレームワーク
組込み / ベアメタルプログラミングで役に立つライブラリやフレームワークを紹介します。
5-1. heaplessクレート
通常、コレクション利用には、グローバルメモリアロケータの実装が必須です (メモリアロケータ参照)。heaplessクレートは、グローバルメモリアロケータがなくても利用できるコレクションです。
heapless
クレートは、Rust 1.36からstableで利用可能になります。
単純にクレートの依存関係を追加し、コレクションをuse
するだけです。
extern crate heapless; // v0.4.x use heapless::Vec; use heapless::consts::*; #[entry] fn main() -> ! { let mut xs: Vec<_, U8> = Vec::new(); xs.push(42).unwrap(); assert_eq!(xs.pop(), Some(42)); }
通常のコレクションと違う点が2つあります。
1つ目は、コレクションの容量を最初に宣言しなければならないことです。heapless
コレクションは固定容量のコレクションです。上のVec
は最大で8つの要素を保持することができます。型シグネチャのU8
が容量を表しています。型シグネチャについては、typenumを参照して下さい。
2つ目は、push
など多くのメソッドがResult
を返すことです。heapless
コレクションは、固定容量を超える要素の挿入は、失敗します。APIは、この操作失敗に対処するために、Result
を返しています。
heapless
コレクションは、通常、スタック上にコレクションを割り当てます。また、static
変数や、ヒープ上に割り当てることも可能です。
v.0.4.4現在、heapless
は次のコレクションを提供しています。
- BinaryHeap: 優先度キュー
- IndexMap: ハッシュテーブル
- IndexSet: ハッシュセット
- LinearMap:
- spsc::Queue: single producer single consumer lock-free queue
- String
- Vec
heaplessの利点
固定容量のコレクションだけを使用して、そのほとんどをstatic
変数に格納し、コールスタックの最大サイズを設定すると、リンカは、物理的に利用可能なメモリより大きな容量を使おうとしたかどうか検出します。
その上、スタックに割り当てられた固定容量のコレクションは、-Z emit-stack-sizesフラグによって報告されます。このフラグは、(stack-sizesのような)スタック使用量を解析するツールがスタック使用量を解析することを意味します。
出典
- The Embedded Rust Book: コレクション
5-2. no_stdクレート
no_stdや組込みで利用可能なクレートを紹介します。Rustのクレートが登録されているcrates.io
には、crates.io No standard libraryカテゴリがあります。2019/5/16現在、840ものクレートが登録されています。
組込みに関連するものは、awesome-embedded-rustに主要なものがまとめられています。
rt (runtime) クレート
rtクレートは、ターゲットアーキテクチャ用の最小限のスタートアップ / ランタイムを提供するクレートです。cortex-m-rt、msp430-rt、riscv-rtの3つのターゲットアーキテクチャに対して実装が存在しています。
これらのクレートは、以下の機能を提供します。
.bss
と.data
セクションの初期化- FPUの初期化
- プログラムのエントリポイントを指定するための
#[entry]
アトリビュート static
変数が初期化される前に呼ばれるコードを指定するための#[pre_init]
アトリビュート- 一般的なターゲットアーキテクチャ用のリンカスクリプト
- ヒープ領域の開始アドレスを表す
_sheap
シンボル
このクレートを使用することで、次のようにアプリケーションのmainコードからプログラムを記述することができます。
#![no_std]
#![no_main]
extern crate panic_halt;
use cortex_m_rt::entry;
// `main`をこのアプリケーションのエントリポイントであるかのように利用できます。
// `main`は戻れません
#[entry]
fn main() -> ! {
// ここに処理を書きます
loop { }
}
Embedonomiconは、このようなrt
クレートの実装方法を解説しています。
embedded HAL
embedded HALは、組込みRustで共通して利用できるtrait
を定義しているクレートです。例えば、SPIやシリアル、といったトレイトが定義されています。
このクレートの抽象を利用して、デバイスドライバを書くことで、アプリケーションの再利用性が向上します。組込みRustの多くのプロジェクトが、このembedded HALを利用しています。
その他
lazy_static
lazy_staticは、実行時にしか初期化できない (new()
関数でのみオブジェクトが構築できる) ような、複雑なオブジェクトのstatic
変数を作るために使います。通常、new()
関数でオブジェクトを作るような構造体は、コンパイル時に値が計算できないため、static
変数の初期化には使えません。また、lazy_static
は、static
変数を1度だけ初期化する機能も提供します。lazy_static
マクロで作られたstatic
変数は、その変数が実行時に最初に使用される時に、初期化されます。
例えば、Writing an OS in RustのVGA Text mode Lazy Staticsでは、VGAにテキストを描画するグローバルインタフェースWRITER
の実装で使用しています。
#![allow(unused)] fn main() { use lazy_static::lazy_static; use spin::Mutex; lazy_static! { pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer { column_position: 0, color_code: ColorCode::new(Color::Yellow, Color::Black), buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, }); } }
Mutex<Writer>
という初期化が非常に複雑なオブジェクトの参照がstatic
変数になっていることがわかります。このように実行時にしか構築できない値もstatic
変数にできる上、どこで初期化するかに悩まなくて済みます。
bitflags
bitflagsは、型安全なビットマスクフラグを提供するクレートです。型安全であることがポイントで、誤ったビット操作を起こしにくいです。AndやOrのオペレータも実装されており、bits()
メソッドで生の値を取り出すことができます。
#[macro_use] extern crate bitflags; bitflags! { struct Flags: u32 { const A = 0b00000001; const B = 0b00000010; const C = 0b00000100; const ABC = Self::A.bits | Self::B.bits | Self::C.bits; } } fn main() { let e1 = Flags::A | Flags::C; let e2 = Flags::B | Flags::C; assert_eq!((e1 | e2), Flags::ABC); // union assert_eq!((e1 & e2), Flags::C); // intersection assert_eq!((e1 - e2), Flags::A); // set difference assert_eq!(!e2, Flags::A); // set complement assert_eq!(e1.bits(), 5u32); // get raw value }
bit_field
bit_fieldは、ビットフィールドへのアクセスを簡単にするためのクレートです。BitField
トレイトを提供しており、i8
, u8
, usize
などの整数型が、トレイトを実装しています。
ビットフィールドへのアクセスは、次のように書けます。
let mut value: u32 = 0b110101;
assert_eq!(value.get_bit(1), false);
assert_eq!(value.get_bit(2), true);
assert_eq!(value.get_bits(2..6), 0b1101);
value.set_bit(2, true);
assert_eq!(value, 0b110111);
value.set_bits(0..2, 0b00);
assert_eq!(value, 0b110100);
bitfield
bitfieldは、ビットフィールドを定義するマクロを提供するクレートです。bit_field
とクレート名が似ていますが、別物です。
下のように、ビットフィールドを定義します。
bitfield! {
#[derive(Clone, Copy, Debug)]
pub struct PinSelect(u32);
pub connected, set_connected: 31;
reserved, _: 30, 6;
pub port, set_port: 5;
pub pin, set_pin: 4, 0;
};
fn main() {
let mut reg = PinSelect(0);
reg.set_pin(5);
reg.set_port(0);
reg.set_connected(1);
assert_eq!(0x1000_0005, reg.all_bits());
}
micromath
micromathは、軽量な数値計算ライブラリです。三角関数などがあります。加速度計など、センサドライバでの計算に利用できます。
register-rs
register-rsは、Rust製のRTOSであるTock
で利用されているMMIO / CPUレジスタインタフェースです。読み書き可能、読み込み専用、書き込み専用、を表現するジェネリック構造体を提供します。
volatile_register
volatile_registerは、メモリマップドレジスタへのvolatileアクセスを提供します。register-rsの簡易版、と言った印象です。
use volatile_register::RW;
// メモリマップドレジスタブロックを表現するstructを作ります
/// Nested Vector Interrupt Controller
#[repr(C)]
pub struct Nvic {
/// Interrupt Set-Enable
pub iser: [RW<u32>; 8],
reserved0: [u32; 24],
/// Interrupt Clear-Enable
pub icer: [RW<u32>; 8],
reserved1: [u32; 24],
// .. more registers ..
}
// ベースアドレスをキャストしてアクセスします
let nvic = 0xE000_E100 as *const Nvic;
// unsafeブロックが必要です
unsafe { (*nvic).iser[0].write(1) }
embedded-graphics
embedded-graphicsは、2Dグラフィックを簡単に描画するためのクレートです。次の機能を提供します。
- 1ビット / ピクセルの画像
- 8ビット / ピクセルの画像
- 16ビット / ピクセル画像
- プリミティブ
- 行、四角、丸、三角
- テキスト
このクレートは、メモリアロケータも事前の巨大なメモリ領域確保も必要としません。
5-3. svd2rust
SVD (System View Description) ファイルからRustのstruct
を自動生成するツールです。SVDファイルはXMLファイルで、特にペリフェラルのメモリマップドレジスタの記述を形式化したものです。
svd2rustは、Cortex-M, MSP430, RISCVのマイクロコントローラに対応しています。svd2rust
で自動生成されたクレートは、PAC (Peripheral Access Crate)
と呼ばれています。主要なPACは、Peripheral Access Cratesにまとめられています。
ちょっとしたプログラムを書く場合、svd2rust
から生成されたPACは間違いを犯しにくいです。svd2rust
で生成されたレジスタアクセス関数では、数値ではなくクロージャを引数に取ります。例えば、GPIOピン (8番ピン) を出力設定にして、highレベルを出力するコードは、次のようになります。
// ピンを出力に設定します
gpioe.moder.modify(|_, w| {
w.moder8().output();
});
// LEDを点灯します
gpioe.odr.write(|w| {
w.odr8().set_bit();
});
クロージャを引数に取る利点は、modify()
メソッドの利用時にあります。modify()
メソッドは、メモリマップドレジスタのリード・モディファイ・ライトを行うAPIです。操作対象のレジスタがクロージャ内でしか操作できないため、別レジスタを誤って操作するような事故が発生しません。
単純なレジスタ読み書きより複雑なコードに見えますが、コンパイラの最適化により、リリースビルドされたバイナリは、通常のレジスタアクセスと同等の機械語になります。
Discoveryでは、svd2rust
で生成したPACを利用して、LEDを点灯したり、シリアル通信します。
後述するRTFM for ARM Cortex-Mでも、svd2rust
で生成したPACを利用します。
5-4. RTFM (Real Time For the Masses)
RTFM for ARM Cortex-Mは、リアルタイムシステムを構築するための並行処理フレームワークです。Real-time for the masses, step 1: Programming API and static priority SRP kernel primitives.というリアルタイムシステム構築の論文を、Rustで実装しています。RTOSほどの機能はありませんが、小規模なリアルタイムシステム構築に向いています。
機能一覧
- 並行処理の単位tとしてタスクが定義されています。タスクはイベントトリガ、もしくは、アプリケーションからspawnすることができます。
- タスク間でメッセージ送受信が可能です。
- ソフトウェアタスクをスケジュールするタイマキューがあります。周期タスクを実装するために利用できます。
- 優先度付きのタスク、および、プリエンプティブマルチタスキングを提供します。
- 優先度に基づいたクリティカルセクション制御により、効率的でデータ競合のないメモリ共有が可能です。
- コンパイル時にデッドロックが発生しないことが保証されます。
- スケジューラは最小限のソフトウェアで実装されており、スケジューリングオーバーヘッドは最小です。
- 全てのタスクが1つのコールスタックを共有しており、極めて効率的にメモリを利用します。
- 全Cortex-Mデバイスをサポートしています。
アプリケーション実装方法
cortex-m-rt
クレートとPeripheral Access Crate (PAC)
に、初期化、タスク、優先度、共有リソースの概念が追加されます。
#[app(device = lm3s6965)]
const APP: () = {
#[init]
fn init() {
// Cortex-M peripherals
let _core: rtfm::Peripherals = core;
// Device specific peripherals
let _device: lm3s6965::Peripherals = device;
// Pends the UART0 interrupt but its handler won't run until *after*
// `init` returns because interrupts are disabled
rtfm::pend(Interrupt::UART0);
hprintln!("init").unwrap();
}
#[idle]
fn idle() -> ! {
// interrupts are enabled again; the `UART0` handler runs at this point
hprintln!("idle").unwrap();
rtfm::pend(Interrupt::UART0);
debug::exit(debug::EXIT_SUCCESS);
loop {}
}
#[interrupt]
fn UART0() {
static mut TIMES: u32 = 0;
// Safe access to local `static mut` variable
*TIMES += 1;
hprintln!(
"UART0 called {} time{}",
*TIMES,
if *TIMES > 1 { "s" } else { "" }
)
.unwrap();
}
};
#[app(..)]
アトリビュートは、device引数を使って、svd2rustで生成されたPACのパスを指定します。#[init]
アトリビュートが指定された関数は、アプリケーションとして実行される最初の関数です。この関数は、割り込み禁止状態で実行します。
coreとdeviceという変数があり、この変数を通して、Cortex-Mとペリフェラルにアクセスできます。
コラム〜RTFMの実装〜
RTFMソースコードを覗いてみると、その多くが手続きマクロによる静的検査と、コードジェネレータであることがわかります。cortex-m-rt
クレートを使用する場合、割り込みとメイン関数間でのデータ共有に制限があります。常に、全ての割り込みを無効化するcortex_m::interrupt::Mutex
を使わなければなりません。しかし、全ての割り込みを無効化することは、常に求められる条件ではありません。
例えば、2つの割り込みハンドラがデータを共有する場合、両者の優先度が同じで、プリエンプションが発生しないとすると、ロックは不要です。RTFMでは、ソースコードを静的に解析することで、不要なロックをせずに、共有データにアクセスできるようになっています。このような解析が可能な理由は、appアトリビュート内にアプリケーションの実装を、全て書くためです。
また、RTFMでは、動的な割り込み優先度の変更をサポートしていません。そのため、全ての割り込みハンドラ間の優先度は静的に決定します。これがうまいこと生きており、ある共有データを使用する割り込みハンドラ同士で、最も優先度の高いハンドラはロックを取得しなくても共有データにアクセスできます。
RTFMは、機能性と安全性を両立するアプローチです。しかし、複雑な手続きマクロで実装されているため、自分で機能を追加したり、アプリケーションをデバッグするのは、骨が折れそうです。
more information
RTFMのドキュメントにRTFMの使い方がまとめられています。
低レイヤ強くなりたい組み込みやさんのブログで、RTRMについていくつかエントリを書きました。タスクの利用方法や共有リソースの管理方法について気になった方は、こちらを参照下さい。
5-5. Tock
TockはRust製の組込みOSです。Cortex-Mアーキテクチャに対応しており、RISC-Vへの移植も進められています。長期に渡り開発が進められており、2018年2月時点でversion 1.0がリリースされています。
主な対応ボード
対応ボードは、既存のRTOSと比較すると多くはありません。一部を紹介します。
- Hail
- TI LAUNCHXL CC26x2 / CC13x2 SimpleLink
- Nordic nRF52x
- STM32 Nucleo
- HiFive1
HiFiveはRISC-Vで、他はARM Cortex-Mが搭載されたボードです。
設計概要
https://github.com/tock/tock/blob/master/doc/tock-stack.pngより
TockのKernelはRustで実装されています。Kernelは2つの階層に分割されています。
1つは、Core kernelでHIL (Hardware Interface Layer) 、スケジューラ、プラットフォーム固有の設定が含まれます。
もう1つは、Capsuleです。Capsuleは、マイコンに依存しないkernel機能を拡張するためのコンポーネント、という位置づけで、通信スタックやコンソールなどが該当します。Capsuleは、unsafe
ブロックの使用が禁止されているなど、Rust固有の安全性を保証する設計がなされています。
Tockでは、CapsuleもユーザプロセスもUntrusted
という扱いですが、その中でも差が設けられています。Capsuleは、kernelのイベントループの中で協調的にスケジューリングされます。そのため、Capsuleがパニックしたり、イベントハンドラに戻らない場合、システムの回復には再起動が必要です。一方で、ユーザプロセスは、MPUでメモリが隔離されており、スケジューリングもプリエンプティブです。
ドキュメント
Tock Documentationに、Tockの設計や実装に関するドキュメントがまとめられています。
Tock Implementationには、Tockの実装についての解説があり、RustでOSを実装する際に参考にできる情報がまとめられています。ここでは、OS実装に必要な要素について、Tock内でどのようにRustで実装しているか、が述べられています。例えば、ライフタイムや可変参照というRust固有の要素をどう扱っているか、メモリアイソレーションやメモリップドレジスタ、システムコールをRustでどのように実装しているか、というトピックが取り上げられています。
ユーザランドアプリケーション
ユーザランドアプリケーションはCとRust、どちらでも書くことができます。C言語用のユーザランドライブラリlibtock-cには、newlib
やlibc++
、lua
ライブラリが含まれます。
libtock-rsは、2019年5月現在、WIP
の状態です。
5-6. テスト
テストを活用することは、組込みやベアメタルの開発でも非常に重要です。しかし、Rustのテストフレームワークは標準ライブラリに依存しており、#[no_std]
環境で使うことができません。ここでは、組込み / ベアメタルRustのプロジェクトで利用されているテストやCIについて紹介します。
デュアルターゲット
部品をcrateに切り出し、ホスト上でテストする方法です。
テスト時には、#![no_std]
でビルドしないようにします。そうすることで、標準ライブラリに依存するRustのテストフレームワークを利用することができます。
lib.rs
でクレートレベルのアトリビュートを次のように指定します。
#![allow(unused)] #![cfg_attr(not(test), no_std)] fn main() { }
これで、テスト時には、#![no_std]
アトリビュートが有効になりません。後は、通常通りテストを書くだけです。heaplessクレートのテストが、参考になります。
カスタムテストフレームワーク
Writing an OS in Rust Testingで紹介されている方法です。unstableなcustom_test_frameworksフィーチャを利用します。
Rust標準のテストフレームワークと比較すると、パニックすることをテストするshould_panic
などの機能が利用できません。
カスタムテストフレームワークを実装するには、次のコードをmain.rs
に追加します。
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
}
test_runner
の引数はFn()
トレイトのトレイトオブジェクトのスライスです。custom_test_frameworksによると、#[test_case]
アトリビュートのついたアイテムが、test_runner
アトリビュートで指定した関数に渡されます。
プロダクトコードのエントリポイントに、テストビルド時のみ、テストハーネスのtest_main
を呼び出すコードを追加します。
#![allow(unused)] #![reexport_test_harness_main = "test_main"] fn main() { #[no_mangle] pub extern "C" fn _start() -> ! { println!("Hello World{}", "!"); #[cfg(test)] test_main(); loop {} } }
テストケースを書きます。
#![allow(unused)] fn main() { #[test_case] fn trivial_assertion() { print!("trivial assertion... "); assert_eq!(1, 1); println!("[ok]"); } }
後は、テストを実行するだけです。
より詳しい情報は、Writing an OS in Rust Testingを参照して下さい。
インテグレーションテスト
QEMUを利用して、特定デバイスのペリフェラルに依存しない試験を実施することができます。RTFM
では、QEMUでバイナリを実行し、semi-hosting機能で標準出力に表示した文字列と期待値とを比較しています。
$ cargo run --example binds
Compiling cortex-m-rtfm v0.5.0-alpha.1
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/examples/binds`
init
foo called 1 time
idle
foo called 2 times
$ cat ci/expected/binds.run
init
foo called 1 time
idle
foo called 2 times
$ cargo run --example binds | diff -u ci/expected/binds.run -
期待通りの動作結果の場合、差分は出力されません。テストの実行結果、差分が出力されるかどうかを検証することで、インテグレーションテストを実施しています。
コンパイルテスト
複雑な (手続き) マクロを利用するクレートでは、様々な利用方法でコンパイルが通るかどうか、をテストします。
RTFM
のtests
ディレクトリにコンパイルテストのテストケースcompiletest.rsがあります。
ここでは、cfail
ディレクトリにコンパイルが失敗するソースファイルが、cpass
にコンパイルが成功するソースファイルが置かれています。
#![allow(unused)] fn main() { use std::{fs, path::PathBuf, process::Command}; use compiletest_rs::{common::Mode, Config}; #[test] fn cfail() { let mut config = Config::default(); config.mode = Mode::CompileFail; config.src_base = PathBuf::from("tests/cfail"); config.link_deps(); // 中略 compiletest_rs::run_tests(&config); } }
コンパイルテストの設定compiletest_rs::Config
を作成し、compiletest_rs::run_tests
でテストを実行します。これで、cfail
ディレクトリ内の全てのRustソースファイルのコンパイルに失敗すると、テストがパス、という扱いになります。
一方、コンパイルが成功するテストは、同テストケース内で次のように実装されています。
#![allow(unused)] fn main() { use tempdir::TempDir; // ... let td = TempDir::new("rtfm").unwrap(); for f in fs::read_dir("tests/cpass").unwrap() { let f = f.unwrap().path(); let name = f.file_stem().unwrap().to_str().unwrap(); assert!(Command::new("rustc") .args(s.split_whitespace()) .arg(f.display().to_string()) .arg("-o") .arg(td.path().join(name).display().to_string()) .arg("-C") .arg("linker=true") .status() .unwrap() .success()); } }
システムコマンドでrustc
を呼び出して、終了ステータスがsuccess
かどうか、をテストしています。
6. FFI (Foreign Function Interface)
組込みRustは急速に環境が整備されつつありますが、まだまだ不足しているものがたくさんあります。そこで、C言語の資産活用が重要になります。本章では、ベアメタル環境でのC言語とのFFIについて説明します。
標準ライブラリが使える場合のFFIについては、実践Rust入門のFFIの章が詳しいです。標準ライブラリ内でC言語とのFFIに使えるモジュールには、std::ffi
とstd::os::raw
があります。残念ながら、どちらのモジュールもcore
には含まれておらず、#![no_std]
環境では利用できません。
代わりに、ctyクレートとcstr_coreクレートとを利用します。
ctyクレートは、コンパイラによって暗黙変換される低レベルのプリミティブ型を扱います。このようなプリミティブ型には、C言語のunsigned int
を表現するc_uint
などがあります。
unsafe fn foo(num: u32) {
let c_num: c_uint = num; // 暗黙変換
}
cstr_coreクレートは、文字列のようなより複雑な型を変換するユーティリティを提供します。
6-1. RustからCを呼ぶ
ここでは、RustのソースコードからC言語のソースコードを呼び出す方法を説明します。やることは2つです。
- CのAPIを、Rustで使えるようにインタフェースを定義する
- Cのコードを、Rustのコードと一緒にビルドする
上記の1.については、bindgenで自動生成できます。2.については、基本的に、Rustのbuild.rs
スクリプトで対応します。
インタフェース定義
まず、手動でインタフェースを定義する例を示します。標準ライブラリが使える環境と異なる点は、cty
クレートを使う点です。
今、次のようなCヘッダファイルが公開されているとします。
/* target.h */
typedef struct MyStruct {
int32_t x;
int32_t y;
} MyStruct;
void my_function(int32_t i, MyStruct* ms);
このヘッダファイルをRustに変換すると、インタフェースは次のようになります。
/* bindings.rs */
#[repr(C)]
pub struct MyStruct {
pub x: cty::int32_t,
pub y: cty::int32_t,
}
pub extern "C" fn my_function(
i: cty::int32_t,
ms: *mut MyStruct
);
#[repr(C)]
アトリビュートにより、Rustコンパイラは、構造体のデータをCと同じルールで構成します。デフォルトでは、Rustコンパイラは、struct
内のデータ順やサイズを保証しません。
pub extern "C" fn my_function(...);
このコードは、my_function
という名前の、C ABIを使った関数を宣言します。関数の定義は、別の場所で与えるか、静的ライブラリから最終バイナリにリンクする必要があります。
インタフェースの自動生成
上述の通り、手動でインタフェースを作成することもできますが、これは単純作業です。そこで、インタフェースを自動生成してくれるbindgen
を利用します。ここでは、一般的な手順を説明し、具体的な例は、ケーススタディ Zephyr bindingsで紹介します。
標準ライブラリが使える環境と、ベアメタル環境とで共通する手順は、次の通りです。
- Rustで使いたいインタフェースやデータ型を定義している全てのCヘッダを集める
- ステップ1で集めたヘッダファイルを
#include "..."
するbinding.h
ファイルを書く bindgen
にbinding.h
を与えて実行するbindgen
の出力を、bindings.rs
にリダイレクトするbindings.rs
をinclude!
するlib.rs
を用意し、クレートとして利用できるようにする- ステップ5で作成したクレートをRustらしく使えるAPIに変換するラッパクレートを作成する
ステップ5で作成する自動生成されたコードのクレートは、-sys
という名前にすることが慣例になっています。
ベアメタル環境でバインディングを作る場合、標準ライブラリが使える環境と異なる点は、次の3点です。
bindgen
利用時に、コマンドラインなら--ctypes-prefix
にcty
オプションを使う、または、ビルドスクリプトならBuilder.ctypes_prefix("cty")
を使う- bindingクレートに
cty
クレートとの依存関係を追加する - bindingクレートに
#![no_std]
アトリビュートを追加する
#[no_std]
環境をターゲットにbindgen
をコマンドラインから利用する場合、例えば次のようなオプションを指定します。
bindgen --use-core --ctypes-prefix cty ...
--use-core
は、標準ライブラリの型を使わずにcore
クレートの型を使うコードを生成します。--ctypes-prefix cty
は、Cの型プレフィックスをcty
クレートの物が使われるようにします。
bindgen
で生成したRustコードを、クレートにまとめます。その際、cty
クレートの依存をCargo.toml
に追加します。
[dependencies]
cty = "0.2.0"
ほぼ定型作業ですが、bindgenで自動生成したコードはbindings.rs
としておき、lib.rs
からインクルードします。lib.rs
には、次の内容を書いておきます。
#![allow(unused)] #![no_std] #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] fn main() { include!("bindings.rs"); }
Cのソースコードは、Rust推奨のコーディングスタイルに沿っていないため、コンパイル時に警告が出ないようにlintルールを緩和します。加えて、#[no_std]
アトリビュートを追加します。
出典
- The Embedded Rust Book: 9.1.Rustと少しのC
6-2. CからRustを呼ぶ
ここでは、CのソースコードからRustのソースコードを呼び出す方法を説明します。やることは2つです。
- Cが扱えるAPIをRust側に作成する
- 外部ビルドシステムにRustプロジェクトを組み込む
上記の1.については、cbindgenで自動生成できます。2.は、Cのプロジェクトやビルドシステムに強く依存するため、一般的な方法はありません。ケーススタディ Zephyr bindingsでは、cmakeプロジェクトに組み込む一例を示します。
ライブラリプロジェクトの作成
通常のRustプロジェクトではなく、システムライブラリを出力します。
[lib]
crate-type = ["cdylib"] # 動的ライブラリ
# crate-type = ["staticlib"] # 静的ライブラリ
C APIの作成
C ABIで呼び出しできるRustのAPIを作成します。おおよそ、次のような関数になります。
#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn rust_function() { // ... } }
no_mangle
Rustコンパイラは、シンボル名をマングルします。そのため、Cから呼び出すRustの関数は、マングルしないように#[no_mangle]
アトリビュートを付けます。
extern "C"
デフォルトでは、Rustの関数はRustのABIを使用します。そこで、CのABIを仕様するように、コンパイラに指示します。プラットフォーム固有のABI指定については、External Blocks ABIにドキュメントがあります。
Cヘッダファイル作成
Rustで作ったAPIをCから呼べるように、Cヘッダファイルを作成します。
#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn rust_function() { ... } }
上のRust APIはCヘッダファイルでは、次のようになります。
void rust_function();
Cヘッダファイルの自動生成
cbindgenにより、RustソースコードからCヘッダファイルを自動生成することができます。cbindgen
をベアメタル環境で使うにあたり注意することは、いくつかの標準ライブラリヘッダをインクルードしたヘッダファイルが生成されることです。
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
void rust_function(void);
ターゲットシステムによってはstdlib.h
が与えられていない可能性があるので、注意して下さい。
外部ビルドシステムにRustプロジェクトを組み込む
Rust APIを作成し、ヘッダファイルを生成すれば、後はCファイルでヘッダをインクルードするだけです。
#include "rust_lib.h"
void call_rust() {
rust_fuction();
}
ビルドシステムへの組み込みについては、Cプロジェクトのビルドシステムが何か、に強く依存します。Makefileでビルドしている場合、ビルドステップの途中でMakefileからcargo
を呼び出し、静的ライブラリとしてRustコードをビルドし、リンクします。
出典
- The Embedded Rust Book: 9.2.Cと少しのRust
6-3. ケーススタディ Zephyr binding
Rust Embedded devices WGでもRTOSとRustとのインテグレーションはissue #62で議論中です。
ここでは、Cで作られたRTOSであるZephyrをターゲットに、RTOSとのインテグレーションを実験してみます。ZephyrのAPIを利用して、Rustからprintln!
マクロを使って、コンソールに文字を出力します。また、RTOSのような複雑なCプロジェクトとのインテグレーションが困難な理由を考察します。
そのために、次のことができるようにします。
- CからRustのAPIを呼び出す
- RustからZephyrのAPIを呼び出す
双方のバインディングは、cbindgen
およびbindgen
を用いて自動生成します。ここで掲載する方法には、まだまだ改善の余地があることに注意して下さい。
次のコードが動くようにします。
#include <rustlib.h>
int main(void) {
rust_main();
}
まずC言語から、Rustのrust_main
関数を呼び出します。バインディング用のヘッダファイルrustlib.h
はcbindgen
で自動生成します。
#[no_mangle]
pub extern "C" fn rust_main() {
println!("Hello {}", "Rust");
}
Rustにはprintln!
マクロを実装します。このprintln!
マクロは、ZephyrのAPIを利用してコンソールに文字列を出力します。Zephyr APIのバインディングは、bindgen
で自動生成します。
最終的に、C (main) → Rust (rust_main) → C (Zephyr API)というコールグラフになります。
環境
これから示すインテグレーション例を試すために必要な環境です。カッコ内は、著者が試したバージョンです。
- Rust (stable 1.35.0)
- cbindgen (0.8.3)
- bindgen (0.49.0)
- Zephyr v.1.14
- Zephyr SDK (0.10.0)
- west (v0.5.8)
- qemu-system-arm (2.11.1)
CからRustのAPIを呼び出す
下のRust関数に対するヘッダファイルを作成します。
#[no_mangle]
pub extern "C" fn rust_main() { /* ... */ }
ビルドスクリプトでも容易に生成できますが、今回はZephyrとのインテグレーション上、Makefileを使う必要があるため、Makefile内で次のコマンドを呼び出します。
cbindgen src/lib.rs -l c -o lib/rustlib.h
これで、次のヘッダファイルが生成されます。
cat hello/lib/rustlib.h
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
void rust_main(void);
このヘッダファイルをCでインクルードします。
#include <rustlib.h>
int main(void) {
rust_main();
}
今回は非常に簡潔です。構造体を引数にしたり、ヒープメモリの管理などリソース管理が加わると、より複雑になります。
RustからZephyrのAPIを呼び出す
こちらの方が難易度が高いです。下準備も色々と必要です。
一般的なライブラリと異なり、多くの組込みOSでは必要最小限の機能だけを組み込んでバイナリを形成します。ビルド時のコンフィギュレーション次第で、ユーザーアプリケーションが利用できるAPIが増減します。そのため、全てのコンフィギュレーションで利用可能なバインディングを作ることは、難しいです。このケーススタディでも、固定のコンフィギュレーションに対してバインディングを作成します。
今回、Zephyrは、デフォルトのZephyr kernel APIに加えて、newlib
という組込み用標準CライブラリのAPIを有効化します。このnewlib
を有効化することにより、C標準ライブラリの一部がZephyrアプリケーションで利用可能になります。
cat prj.config
# General config
CONFIG_NEWLIB_LIBC=y
この設定により、次のようなC標準ライブラリAPIが利用可能になります。
- I/O
- printf, fwrite, etc
- 文字列
- strtoul, atoi, etc
- メモリ
- malloc, free, etc
これらAPIのバインディングを自動生成しつつ、Rustっぽく使えるようにラッピングしていきます。
バインディングの自動生成
まず、第一関門です。ここで難しい点は、2つあります。
- OSのコンフィギュレーションによって利用できるAPIが異なる
- 一部APIがZephyrのビルドシステムで自動生成するヘッダに依存している
上記理由から、一度ターゲットとするZephyrをビルドした後で、bindgen
を使用することにしました。まず、空のアプリケーションを用意して、Zephyrをビルドします。
cat src/main.c
int main(void) {
return;
}
# Zephyrプロジェクトのディレクトリ
source zephyr-env.sh
# ケーススタディプロジェクトのディレクトリ
west build -b cortex_qemu_m3 hello
これで、build/zephyr/include.generated
に必要なヘッダファイルが生成されます。Zephyr
の環境変数を利用しながら、bindgen
でバインディングを生成します。
$ bindgen --use-core --ctypes-prefix cty zephyr-sys/headers/bindings.h -o zephyr-sys/src/bindings.rs -- -I${ZEPHYR_BASE}/include -I${ZEPHYR_BASE}/arch/arm/include -I./build/zephyr/include/generated -m32
前述の通り、std
クレートにあるFFI型は利用できないため、cty
クレートを使います。--ctypes-prefix cty
の部分です。
バインディングを生成するためのヘッダファイルは以下の通りです。
cat zephyr-sys/headers/bindings.h
#include <autoconf.h>
#include <stdio.h>
Zephyrのビルドシステムで自動生成されるヘッダautoconf.h
内には、アーキテクチャ依存のマクロ定義など、重要な定義が数多く含まれます。Zephyrのヘッダファイルおよびソースファイルは、このautoconf.h
に含まれるマクロが定義されていることが前提になっています。そこで、バインディング作成時にも、まず最初にautoconf.h
をインクルードしています。
--
以降 (-I${ZEPHYR_BASE}
から後) のオプションは、clang
に与えるオプションです。-I
でインクルードパスを、-m32
でターゲットアーキテクチャが32ビットであることを指示しています。
これで、bindings.rs
を得ます。例えば、printf
のバインディングは、次のように生成されています。
extern "C" {
pub fn printf(fmt: *const cty::c_char, ...) -> cty::c_int;
}
このbindings.rs
をクレートとして利用できるようにします。次のlib.rs
を作成します。
#![no_std]
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
include!("bindings.rs");
Rustとしてはあくまでのno_std
な環境となるため、#![no_std]
アトリビュートが必要です。バインディングを作成したCソースファイルは、Rustの命名規則に沿っていません。そこで、Rustコンパイラのlintで警告が出ないように、命名規則のlintルールを除外しています。
lib.rs
とbindings.rs
をzephyr-sys
クレートとしてまとめれば、バインディングの生成は完了です。
ラッピングクレートの作成
zephyr-sys
クレートのバインディングは、C APIをそのまま変換しただけなので、Rustらしい安全で使いやすいAPIになっていません。例えば、printf
をRustから呼び出そうとすると、次のようなコードになります。
unsafe {
zephyr::printf(b"Hello from %s\0".as_ptr() as *const cty::c_char,
b"Rust\0".as_ptr());
}
お世辞にも使いやすいとは言えません。その上、文字列をナル文字 (\0
) で終端し忘れると、未定義動作に突入します。
そこで、zephyr-sys
をラッピングして、Rustらしく使えるAPIを実装します。ここが、Cとのインテグレーションで一番大変なところです。
ここでは、Zephyrのnewlib
APIを使って、アプリケーションから安全に利用できるprintln!
マクロを作ります。ベースは、print!マクロで実装したマクロと同じです。異なる点は、fmt::Write
トレイトのwrite_str
メソッドの実装で、Zephyrのfwrite
を呼び出す点です。
/// Pseudo writer which uses Zephyr `fwrite` API.
/// Because `fwrite` does not guarantee its atomicity, this wrapper
/// does not provide any lock mechanism.
pub struct DebugWriter {}
impl fmt::Write for DebugWriter {
fn write_str(&mut self, s: &str) -> fmt::Result {
// safe: `fwrite` does not need to guarantee the atomicity.
unsafe {
zephyr_sys::fwrite(s.as_ptr() as *const cty::c_void, s.len(), 1, stdout_as_ptr_mut());
}
Ok(())
}
}
今回の実装では、マルチスレッドで動作した場合のアトミック性については考慮しません。Zephyrのfwrite
API自体がアトミック性を保証していないため、Rust側で工夫をしても、システム全体のアトミック性が保証できないためです。
もし、C APIが (例えばMutexなどにより) アトミック性を保証する仕組みを持つ場合、DebugWriter
を空実装にせず、然るべきロック機能を持たせると良いでしょう。
今回、fwrite
を用いてprintln!
マクロを実装した理由は、ランタイムコストを減らすためです。別解として、printf
を用いる実装が考えられますが、文字列のフォーマットはRust処理系で安全性が保証されているため、Cのフォーマットを使う理由がありません。それどころか、printf
のフォーマット処理分、余分なランタイムコストがかかります。
Rustのstr
は、バイト数を取得するlen
メソッドを備えているため、stdout
に対して、指定バイト数書き込む実装にすると、ランタイムコストが少なくなります。
最後になりますが、このクレートをzephyr
クレートとします。Cargo.toml
ファイルにzephyr-sys
クレートへの依存関係を追加します。
[dependencies]
cty = "0.2.0"
zephyr-sys = { path = "../zephyr-sys" }
アプリケーション作成
アプリケーションは、上記で作成したprintln!
マクロを呼び出すだけです。
use zephyr::{print, println};
#[no_mangle]
pub extern "C" fn rust_main() {
println!("Hello {}", "Rust");
}
このクレートをhello
クレートとして、zephyr
クレートへの依存を追加します。
[dependencies]
cty = "0.2.0"
zephyr = { path = "../zephyr" }
このクレートは、staticライブラリとしてビルドし、Zephyrとリンクします。
[lib]
name = "rustlib"
crate-type = ["staticlib"]
このクレートは、Makefile
を使ってビルドします。これは主にZephyrとのインテグレーション上の理由です。おおよそ、次のコマンドが実行されるようにMakefile
を作成します。
cargo build
cargo objcopy -- --weaken lib/librustlib.a
cbindgen src/lib.rs -l c -o lib/rustlib.h
cargo
でプロジェクトのstaticライブラリを作成し、objcopy
でシンボルをweak
にします。これは、Rustコンパイラビルトインのmemcopy
やmemset
といった関数が、Zephyrのシンボルと衝突してしまうためです。
余談ですが、現状、Rustにはビルド後のバイナリやライブラリを操作するためのポストビルドスクリプトの仕組みがありません。 今回のように、
objcopy
などを使いたい場合には、Makefile
など外部ビルドシステムに依存しなければなりません。
また、このクレートはCから呼び出されるため、Cのバインディングを生成します (先述の通り)。
ビルドシステムへのインテグレーション
最後の仕事です。ZephyrはビルドシステムにCMake
を採用しています。Zephyrのビルドプロセス中で外部ビルドシステムを呼び出し、ライブラリをリンクする方法が確立されています。
詳細な説明は省略しますが、cmake
のExternalProject
を用いて、CMakeLists.txt
を次の通り記述します。
# Include External Rust Library
include(ExternalProject)
set(rust_prj hello)
set(rust_src_dir ${CMAKE_CURRENT_SOURCE_DIR}/${rust_prj})
set(rust_build_dir ${CMAKE_CURRENT_BINARY_DIR}/${rust_prj})
ExternalProject_Add(
rust_project # Name for custom target
PREFIX ${rust_build_dir} # Root dir for entire project
SOURCE_DIR ${rust_src_dir}
BINARY_DIR ${rust_src_dir} # This particular build system is invoked from the root
CONFIGURE_COMMAND "" # Skip configuring the project, e.g. with autoconf
BUILD_COMMAND make
INSTALL_COMMAND "" # This particular build system has no install command
BUILD_BYPRODUCTS ${rust_src_dir}/lib/librust.a
)
add_library(rust_lib STATIC IMPORTED GLOBAL)
add_dependencies(
rust_lib
rust_project
)
set_target_properties(rust_lib PROPERTIES IMPORTED_LOCATION ${rust_src_dir}/lib/librust.a)
set_target_properties(rust_lib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${rust_src_dir}/lib)
target_link_libraries(app PUBLIC rust_lib)
今回は、先の手順で作成したMakefile
を呼び出す形にしました。これでようやく全ての準備が整いました。
動作確認
次のCアプリケーションを作成して、src/main.c
とします。
#include <stdio.h>
int main(void) {
rust_main();
}
次のコマンドで今回のプロジェクトをビルドし、QEMUで実行できます。
mkdir build && cd $_
cmake -GNinja -DBOARD=qemu_cortex_m3 ..
ninja run
実行すると無事にHello Rust
が表示されます。
To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
***** Booting Zephyr OS zephyr-v1.14.0 *****
Hello Rust
考察
この通り、RTOSのような複雑なCプロジェクトとRustとのインテグレーションには、特有の困難さがあります。まず、ラッピングするAPIの数が膨大です。今回は、単一機能に対してのみラッピングAPIを作成しましたが、これをRTOSがアプリケーションに提供するAPIの数分こなさなければなりません。
加えて、RTOSではターゲットシステムごとに、アプリケーションが利用できるAPIが増減します。この要素をどのように統一的に扱うか、を解決する必要があります。今後のコミュニティの動きに注目しましょう。
コラム〜マイコン上でRustで書いたWASMアプリケーションが動く!?〜
2019年5月、WebAssembly Micro Runtimeというマイコン上で動作するWASMランタイムが公開されました。このWASMランタイムは、Zephyr上で動かすことができます。
それほど苦労せずに、256KBのRAMが搭載されているマイコン上でRustのアプリケーションを実行できました。アプリケーション実行までの簡単な手順をWebAssembly Micro RuntimeでRustアプリをマイコンで動かす!で公開しています。ランタイムの性能が気になるところですが、WASMが動くようになればターゲットアーキテクチャを気にしなくてもRustアプリケーションが動かせるようになるため、今後もWASM Micro Runtimeに注目しましょう。
参考
7. 組込みLinux
組込み開発でも比較的リッチなシステムでは、Linuxを採用します。ここでは、組込みLinuxのアプリケーションをRustで作成する方法を説明します。
Rustコンパイラがサポートしているターゲットであれば、難しいことはほとんどありません。コンパイラのターゲットをインストールし、--target
オプションで指定するだけです。
ここでは、Raspberry Pi3向けにクロスビルドする方法、テストの実行方法、Yoctoに組み込んでのビルドについて説明します。Raspberry Pi3をお持ちでない方向けに、QEMUでの動作確認方法も記載しています。
7-1. ビルド
32ビットのARMv7
と、64ビットのAArch64
とでビルドできる手順をそれぞれ示します。Raspbianを使用している場合は、32ビットのARMv7
をターゲットにして下さい。
Ubuntu 18.04
で動作するコマンドを掲載しています。他のOSをご利用の方は、お手数ですが読み替えをお願いします。
環境構築
まずクロスコンパイルのターゲットをインストールします。
# ARMv7
rustup target add armv7-unknown-linux-gnueabihf
# AArch64
rustup target add aarch64-unknown-linux-gnu
Rustコンパイラでは、ネイティブ用のリンカしか配布していないため、リンカは別途用意します。Yoctoなどでツールチェインを構築している場合、そのツールチェインを利用できます。
# ARMv7
sudo apt install g++-arm-linux-gnueabihf
# AArch64
sudo apt install g++-aarch64-linux-gnu
.cargo/config
でリンカを指定します。
# ARMv7
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
# AArch64
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
プロジェクトをビルドする際は、次の通り、ターゲットを指定します。
# ARMv7
cargo build --target=armv7-unknown-linux-gnueabihf
# AArch64
cargo build --target=aarch64-linux-gnu-gcc
生成されたバイナリ (target/armv7-unknown-linux-gnueabihf/debug/
またはtarget/aarch64-unknown-linux-gnu/debug/
にあります) をRaspberry Pi3にコピーするだけで、実行できます。ターゲットシステム上のライブラリに依存する場合は、ライブラリパスなどを別途、指定する必要があります。
QEMUのユーザーモードエミュレーションを使って、動作確認してみましょう。
sudo apt install qemu-user-binfmt
ダイナミックリンクしているのでクロスルートディレクトリを明示的に指定します。
# ARMv7
$ qemu-arm -L /usr/arm-linux-gnueabihf target/armv7-unknown-linux-gnueabihf/debug/raspi
Hello, world!
# AArch64
qemu-aarch64 -L /usr/aarch64-linux-gnu/ target/aarch64-unknown-linux-gnu/debug/raspi
Hello, world!
.cargo/config
にカスタムランナーを設定することで、cargo run
でQEMU上やRaspberry Pi3上で実行することができます。まず、QEMU上で実行する設定です。
cat .cargo/config
# ARMv7
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
runner = "qemu-arm -L /usr/arm-linux-gnueabihf"
# AArch64
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64 -L /usr/aarch64-linux-gnu"
# ARMv7
cargo run --target armv7-unknown-linux-gnueabihf
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `qemu-arm -L /usr/arm-linux-gnueabihf target/armv7-unknown-linux-gnueabihf/debug/raspi`
Hello, world!
# AArch64
cargo run --target aarch64-unknown-linux-gnu
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `qemu-aarch64 -L /usr/aarch64-linux-gnu target/aarch64-unknown-linux-gnu/debug/raspi`
Hello, world!
バイナリサイズを気にしない場合、musl
のターゲットを利用すると、ターゲット環境のlibcに依存しないバイナリを生成することができます。その場合、armv7-unknown-linux-musleabihf
もしくはaarch64-unknown-linux-musl
を指定します。Hello World
プログラムで、armv7-unknown-linux-gnueabihf
は約1.5 MB、armv7-unknown-linux-musleabihf
は約1.8 MBになります。
続いて、カスタムランナーを設定して、リモートのRaspberry Pi3でバイナリを実行する例を示します。以降では、パスワード認証方式でsshすることを想定しています。Raspberry Pi3にsshするための設定は、事前に済ませて下さい。公開鍵認証方式を使用してもかまいませんし、開発期間の間はパスワードなしでssh可能にしても良いです。
shell script内でパスワードを入力するためexpect
をインストールします。
sudo apt install expect
次のシェルスクリプトを用意します。
$ cat run.sh
#!/bin/sh
PW="raspberry"
expect -c "
set timeout 5
spawn env LANG=C /usr/bin/scp $1 pi@<IPアドレス>:/home/pi/raspi
expect \"password:\"
send \"${PW}\n\"
expect \"$\"
spawn env LANG=C /usr/bin/ssh pi@<IPアドレス> ./raspi
expect \"password:\"
send \"${PW}\n\"
expect \"$\"
exit 0
"
shell script実行時の第一引数を、/home/pi/
にraspi
としてコピーします。次に、Raspberry Pi3上のraspi
バイナリを実行します。
これをプロジェクトのルートディレクトリ (Cargo.tomlのあるディレクトリ) に置いて、カスタムランナーに指定します。
# ARMv7
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
runner = "sh run.sh"
Raspberry Pi3にssh可能な状態で、cargo run
を実行します。
cargo run --target armv7-unknown-linux-gnueabihf
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `sh run.sh target/armv7-unknown-linux-gnueabihf/debug/raspi`
spawn env LANG=C /usr/bin/scp target/armv7-unknown-linux-gnueabihf/debug/raspi pi@<IPアドレス>:/home/pi/raspi
pi@<IPアドレス>'s password:
raspi 100% 1481KB 1.3MB/s 00:01
spawn env LANG=C /usr/bin/ssh pi@<IPアドレス> ./raspi
pi@<IPアドレス>'s password:
Hello, world!
テスト
上述のカスタムランナーを登録しておけば、cargo test
でQEMU上やRaspberry Pi3上でテストを実行可能です。
QEMU上での実行例です。
cargo test --target armv7-unknown-linux-gnueabihf
Compiling raspi v0.1.0 (embedded-rust-techniques/ci/07-linux/raspi)
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
Running target/armv7-unknown-linux-gnueabihf/debug/deps/raspi-3f64731a0be9b753
running 1 test
test ok ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Raspberry Pi3上での実行例です。
cargo test --target armv7-unknown-linux-gnueabihf
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/armv7-unknown-linux-gnueabihf/debug/deps/raspi-2fd52b9957ada715
spawn env LANG=C /usr/bin/scp embedded-rust-techniques/ci/07-linux/raspi/target/armv7-unknown-linux-gnueabihf/debug/deps/raspi-2fd52b9957ada715 pi@<IPアドレス>:/home/pi/raspi
pi@<IPアドレス>'s password:
raspi-2fd52b9957ada715 100% 2820KB 998.2KB/s 00:02
spawn env LANG=C /usr/bin/ssh pi@<IPアドレス> ./raspi
pi@<IPアドレス>'s password:
running 1 test
test ok ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
7-2. Yocto
Yoctoは、組込みLinuxディストリビューションを作成するためのプロジェクトです。製品固有のLinuxディストリビューションを作成、管理できるため、組込みLinux開発で広く用いられています。ここで言うLinuxディストリビューションは、Linux kernel、ライブラリ、アプリケーションを全て含みます。
日本語のまとまった書籍は、2019年現在ありませんが、雑誌インタフェースなどで、Raspberry Piの独自環境構築や、FPGAボードZynqの環境構築方法が紹介されています。みつきんのメモは、Yocto関連のノウハウが多く掲載されており、普段からお世話になっています。
ここでは、RustプロジェクトをYoctoビルド環境にインテグレーションする方法を紹介します。想定する利用方法は、ホスト上のRustツールチェインで一通り開発、デバッグを行った上で、distributionに取り込んで配布する、というものです。
Yoctoの基礎から説明するスキルが著者にないため、Yoctoを触ったことある方向けの情報になります。ご了承下さい。
ターゲット環境はRaspberry Pi3で、Yoctoのバージョンはthud
です。
meta-rust
meta-rustは、既存のRustプロジェクトをYoctoでビルドできるようにするための、Yoctoのレイヤです。
まず、raspberry pi3環境をビルドするためのレイヤを取得します。
mkdir -p rpi-thud/layers
cd rpi-thud/layers
git clone git://git.yoctoproject.org/poky.git -b thud
git clone git://git.yoctoproject.org/meta-raspberrypi -b thud
git clone git://git.openembedded.org/meta-openembedded -b thud
次に、Rustのパッケージを含んでいるmeta-rust
をcloneします。
git clone https://github.com/meta-rust/meta-rust.git
環境変数を読み込みます。
source layers/poky/oe-init-build-env build
ビルド対象のレイヤを追加します。
bitbake-layers add-layer ../layers/meta-openembedded/meta-oe
bitbake-layers add-layer ../layers/meta-openembedded/meta-python
bitbake-layers add-layer ../layers/meta-openembedded/meta-networking
bitbake-layers add-layer ../layers/meta-raspberrypi
bitbake-layers add-layer ../layers/meta-rust
local.conf
を修正します。
ターゲットをRaspberry Pi3にします。
MACHINE = "raspberrypi3"
Rustのサンプルパッケージをrootfsにインストールするようにします。
IMAGE_INSTALL_append = " rust-hello-world"
ビルドします。
bitbake core-image-base
dd
コマンドでマイクロSDカードにイメージを書き込みます。
sudo dd if=tmp/deploy/images/raspberrypi3/core-image-base-raspberrypi3.rpi-sdimg of=/dev/sdX bs=100M
/sdX
は使用している環境に合わせて適宜変更して下さい。
raspberry pi3を起動して、rust-hello-world
を実行します。
# rust-hello-world
Hello, world!
無事、実行できます。
cargo-bitbake
cargo-bitbakeは、既存のCargoプロジェクトからmeta-rust
のYoctoレシピを作成してくれるCargoの拡張機能です。
cargo-bitbake
はlibssl-dev
を使用するため、インストールします。
sudo apt install libssl-dev
cargo-bitbake
をインストールします。
cargo install cargo-bitbake
Rust製grepツールのripgrepを取り込んでみます。
git clone https://github.com/BurntSushi/ripgrep.git
cd ripgrep
cargo bitbake
これで、レシピが自動生成されます。
head ripgrep_11.0.1.bb
# Auto-Generated by cargo-bitbake 0.3.10
#
inherit cargo
# If this is git based prefer versioned ones if they exist
# DEFAULT_PREFERENCE = "-1"
# how to get ripgrep could be as easy as but default to a git checkout:
# SRC_URI += "crate://crates.io/ripgrep/11.0.1"
SRC_URI += "git://github.com/BurntSushi/ripgrep.git;protocol=https"
LIC_FILES_CHKSUM
だけは、手動で変更する必要があります。
# FIXME: update generateme with the real MD5 of the license file
LIC_FILES_CHKSUM=" \
file://Unlicense OR MIT;md5=generateme \
"
ripgrep
では、COPYING
ファイルにライセンス情報が記載されています。md5sum
コマンドでチェックサムを計算して、レシピを修正します。
md5sum COPYING
034e2d49ef70c35b64be514bef39415a COPYING
レシピは次のようになります。
LIC_FILES_CHKSUM=" \
file://COPYING;md5=034e2d49ef70c35b64be514bef39415a \
"
layers/meta-rust/recipes-example/ripgrep/
ディレクトリを作成し、自動生成されたレシピファイルをコピーします。
mkdir ../layers/meta-rust/recipes-example/ripgrep/
cp <path to ripgrep>/ripgrep_11.0.1.bb ../layers/meta-rust/recipes-example/ripgrep/
ビルドします。
bitbake ripgrep
これで、ripgrep
がビルドできます。
meta-rust-bin
meta-rust
では、LLVM、Rustコンパイラ、CargoをビルドしてRustツールチェインを構築するため、ビルド時間が大幅に増加します。それにも関わらず、Yoctoで作成したクロス開発環境には、このツールチェインが含まれません。純粋に、Rustのプロジェクトをビルドするだけであれば、既存のRustツールチェインバイナリを取得する方がよほどお手軽です。
そこで、Rustのツールチェインバイナリを取得して、Rustプロジェクトをビルドするmeta-rust-binがあります。
meta-rust
と異なり、こちらは、pokyのバージョンがsumo
までしか対応されていません (2019/6/22現在)。
mkdir -p rpi-sumo/layers
cd rpi-sumo/layers
git clone git://git.yoctoproject.org/poky.git -b sumo
git clone git://git.yoctoproject.org/meta-raspberrypi -b sumo
git clone git://git.openembedded.org/meta-openembedded -b sumo
git clone https://github.com/rust-embedded/meta-rust-bin
環境変数を読み込みます。
source layers/poky/oe-init-build-env build
ビルド対象のレイヤを追加します。
bitbake-layers add-layer ../layers/meta-openembedded/meta-oe
bitbake-layers add-layer ../layers/meta-openembedded/meta-python
bitbake-layers add-layer ../layers/meta-openembedded/meta-networking
bitbake-layers add-layer ../layers/meta-raspberrypi
bitbake-layers add-layer ../layers/meta-rust-bin
meta-rust-bin
には、レシピ例が同梱されていないため、サンプルアプリのレシピを作成します。meta-rust
のrust-hello-world
レシピがそのまま利用できます。
cp -r <path to meta-rust>/recipes-example/rust-hello-world/ ../layers/meta-rust-bin/
local.conf
を修正します。
ターゲットをRaspberry Pi3にします。
MACHINE = "raspberrypi3"
Rustのサンプルパッケージをrootfsにインストールするようにします。
IMAGE_INSTALL_append = " rust-hello-world"
ビルドします。
bitbake core-image-base
dd
コマンドでマイクロSDカードにイメージを書き込みます。
sudo dd if=tmp/deploy/images/raspberrypi3/core-image-base-raspberrypi3.rpi-sdimg of=/dev/sdX bs=100M
/sdX
は使用している環境に合わせて適宜変更して下さい。
raspberry pi3を起動して、rust-hello-world
を実行します。
# rust-hello-world
Hello, world!
無事、実行できます。
meta-rust-bin
でもripgrep
をビルドしてみます。レシピの用意は簡単です。
inherit cargo
SUMMARY = "ripgrep recursively searches directories for a regex pattern"
HOMEPAGE = "https://github.com/BurntSushi/ripgrep"
LICENSE = "MIT"
SRC_URI = "git://github.com/BurntSushi/ripgrep.git;tag=${PV};protocol=https"
S = "${WORKDIR}/git"
LIC_FILES_CHKSUM = "file://LICENSE-MIT;md5=8d0d0aa488af0ab9aafa3b85a7fc8e12"
bitbake ripgrep
これで、ripgrep
がビルドできます。
meta-rustとの比較
ラズパイ3のcore-image-base
にrust-hello-world
を追加したイメージのフルビルドにかかる時間を計測したろころ、meta-rust
が約220分、meta-rust-bin
が約75分でした。Yoctoのバージョンが異なるため、完全なベンチマークとは言えませんが、meta-rust-bin
の方がビルド時間がかなり短いです。
meta-rust-bin
はビルド済みのRustツールチェインを利用するため、Rustコンパイラをカスタマイズしてビルドする、ということができません。Rustが公式にサポートしていないアーキテクチャをターゲットにする場合は、meta-rust
の利用が必要です。
また、ビルド済みのRust標準ライブラリを利用するため、カスタムビルドされた標準ライブラリよりパフォーマンスが低い可能性があります。
cargo-bitbake
で自動生成するレシピは、meta-rust-bin
のclassとは互換性がありません。meta-rust-bin
のレシピを用意するのは、それほど難しくないため、大きなデメリットではありません。