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. 参考文献

本書に含まれない内容について学習したい場合に、参考となる書籍を紹介します。

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 

ここで、ハマりどころがあります。

  1. Rustがサポートするターゲットシステムの一覧がわからない
  2. ターゲットシステムがサポートされていない

これらの詳細は、コンパイラサポートに記載しますが、解決方法を簡単にだけ示します。まず、ターゲットシステム一覧は、次のコマンドで取得できます。

$ 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-objdumpllvm-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クレートとリンクします。

出典

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(())
    }
}

フォーマット文字列を取り扱うために、UartWritercore::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

出典

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にしてデフォルト実装を与えたり、明示的に外部リンケージにすることができます。

出典

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"));

出典

3-6. メモリアロケータ

ベアメタルプログラミングでも開発が進むと動的なコレクションが使いたくなります。stdが使える通常のRustでは、VecStringといった一般的なコレクションが利用できます。このようなコレクションは、ヒープメモリを利用します。そのため、デフォルトでは、no_stdな環境では、これらのコレクションを利用できません。しかし、no_stdなRustでも、メモリアロケータを実装することで、コレクションを利用することができます。

メモリアロケータを実装せずにコレクションを利用する方法は、heaplessで説明します。

ただ、(執筆時点のRust 1.35.0では) 残念なことにnightly必須です。ベアメタルでメモリアロケータを実装するには、allocalloc_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!マクロで説明しています。

グローバルアロケータ

VecStringといったコレクションは、デフォルトではグローバルアロケータを使ってヒープメモリ領域を確保します。グローバルアロケータとは、#[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を持ちます。headUnsafeCellになっている理由は、&selfを引数に取るallocメソッドの中でheadの値を書き換えるためです。allocメソッドのシグネチャは、GlobalAllocトレイトで定義されているため、引数を&mut selfに変更することができません。

unsafe impl Sync for BumpPointerAlloc {}

次にSyncトレイトを実装します。これは、グローバルアロケータのオブジェクトがstatic変数になるため、スレッド間で安全に共有できることをコンパイラに伝えるためです。

GlobalAllocトレイトの実装で求められるメソッドは、allocdeallocのみです。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を書くプロジェクトです。このプロジェクトでは、kmallockfreeをFFIで呼び出し、Linux kernelの機能を用いてRustのメモリアロケータを実装します。

出典

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では、タイムアウトでプロセスが強制終了されますが、コンパイルが通ることがわかります。

出典

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 階層構造を参照して下さい。

組込み / ベアメタルでよく使う設定項目は、targetbuildです。

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-eabicargo 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

組込み / ベアメタルの開発において、バイナリを調査することは、息をするより自然なことです。バイナリの調査を行う際、objdumpsizereadelfnmなどのツールを利用します。GNUのbinutilsを使用しても良いのですが、LLVMのものを利用すると、rustcがサポートするターゲットアーキテクチャ全てに対応しており、便利です。

Cargoのサブコマンドであるcargo binutilsが提供されており、Cargoからobjdumpsizeコマンドを利用できます。

インストールも非常に簡単です。

$ 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

amdgcnavrxcoreなど、Rustコンパイラではサポートされていないアーキテクチャがあります。Rustコンパイラではこれらのアーキテクチャサポートが無効化されて、配布されています。

Rustがサポートしていないターゲットのビルド

ここから先は、著者が試したことがないため、参考情報となります。

もし使用したいターゲットが、Rustコンパイラで無効化されている場合 (上述のamdcgnavr) 、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

出典

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つに分類されます。

  1. allow (許可)
  2. warn (警告)
  3. deny (拒絶)
  4. 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は275
  • opt-level = 2は225
  • opt-level = "s"は75
  • opt-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のような)スタック使用量を解析するツールがスタック使用量を解析することを意味します。

出典

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-rtmsp430-rtriscv-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には、newliblibc++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 -

期待通りの動作結果の場合、差分は出力されません。テストの実行結果、差分が出力されるかどうかを検証することで、インテグレーションテストを実施しています。

コンパイルテスト

複雑な (手続き) マクロを利用するクレートでは、様々な利用方法でコンパイルが通るかどうか、をテストします。

RTFMtestsディレクトリにコンパイルテストのテストケース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::ffistd::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つです。

  1. CのAPIを、Rustで使えるようにインタフェースを定義する
  2. 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で紹介します。

標準ライブラリが使える環境と、ベアメタル環境とで共通する手順は、次の通りです。

  1. Rustで使いたいインタフェースやデータ型を定義している全てのCヘッダを集める
  2. ステップ1で集めたヘッダファイルを#include "..."するbinding.hファイルを書く
  3. bindgenbinding.hを与えて実行する
  4. bindgenの出力を、bindings.rsにリダイレクトする
  5. bindings.rsinclude!するlib.rsを用意し、クレートとして利用できるようにする
  6. ステップ5で作成したクレートをRustらしく使えるAPIに変換するラッパクレートを作成する

ステップ5で作成する自動生成されたコードのクレートは、-sysという名前にすることが慣例になっています。

ベアメタル環境でバインディングを作る場合、標準ライブラリが使える環境と異なる点は、次の3点です。

  1. bindgen利用時に、コマンドラインなら--ctypes-prefixctyオプションを使う、または、ビルドスクリプトならBuilder.ctypes_prefix("cty")を使う
  2. bindingクレートにctyクレートとの依存関係を追加する
  3. 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]アトリビュートを追加します。

出典

6-2. CからRustを呼ぶ

ここでは、CのソースコードからRustのソースコードを呼び出す方法を説明します。やることは2つです。

  1. Cが扱えるAPIをRust側に作成する
  2. 外部ビルドシステムに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コードをビルドし、リンクします。

出典

6-3. ケーススタディ Zephyr binding

Rust Embedded devices WGでもRTOSとRustとのインテグレーションはissue #62で議論中です。

ここでは、Cで作られたRTOSであるZephyrをターゲットに、RTOSとのインテグレーションを実験してみます。ZephyrのAPIを利用して、Rustからprintln!マクロを使って、コンソールに文字を出力します。また、RTOSのような複雑なCプロジェクトとのインテグレーションが困難な理由を考察します。

そのために、次のことができるようにします。

  1. CからRustのAPIを呼び出す
  2. RustからZephyrのAPIを呼び出す

双方のバインディングは、cbindgenおよびbindgenを用いて自動生成します。ここで掲載する方法には、まだまだ改善の余地があることに注意して下さい。

次のコードが動くようにします。

#include <rustlib.h>

int main(void) {
	rust_main();
}

まずC言語から、Rustのrust_main関数を呼び出します。バインディング用のヘッダファイルrustlib.hcbindgenで自動生成します。

#[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つあります。

  1. OSのコンフィギュレーションによって利用できるAPIが異なる
  2. 一部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.rsbindings.rszephyr-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コンパイラビルトインのmemcopymemsetといった関数が、Zephyrのシンボルと衝突してしまうためです。

余談ですが、現状、Rustにはビルド後のバイナリやライブラリを操作するためのポストビルドスクリプトの仕組みがありません。 今回のように、objcopyなどを使いたい場合には、Makefileなど外部ビルドシステムに依存しなければなりません。

また、このクレートはCから呼び出されるため、Cのバインディングを生成します (先述の通り)。

ビルドシステムへのインテグレーション

最後の仕事です。ZephyrはビルドシステムにCMakeを採用しています。Zephyrのビルドプロセス中で外部ビルドシステムを呼び出し、ライブラリをリンクする方法が確立されています。

詳細な説明は省略しますが、cmakeExternalProjectを用いて、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に注目しましょう。

参考

freertos.rs

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-bitbakelibssl-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-rustrust-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-baserust-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のレシピを用意するのは、それほど難しくないため、大きなデメリットではありません。

参考