Rustでプラットフォーム依存の処理を書く

#[cfg(...)] Attribute

Rustでは #[cfg(...)] アトリビュートを使うことによって、OSやCPUに応じた条件コンパイルを行うことができる。

例えば、次のように書くことでコンパイルターゲットのOSに応じたhello()をビルドし、呼び出すことができる。

#[cfg(target_os = "windows")]
fn hello() { println!("Hello, I'm Windows!"); }

#[cfg(target_os = "linux")]
fn hello() { println!("Hello, I'm Linux!"); }

#[cfg(target_os = "macos")]
fn hello() { println!("hello, I'm MacOS."); }

fn main() {
    hello();
}

モジュールを分ける

各複数プラットフォーム向けの処理が小さい時には、前述のように1つのファイルの中にベタ書きでも問題ないが、コードが膨らんできた場合は各プラットフォーム毎にファイルを分けた方がいいだろう。

そこで、次のようなモジュール構成を考てみる。 nameモジュールを用意し、OS依存部分はnameモジュールの中に隠蔽することを目的とする。

Cargo.toml
src/
├── lib.rs
└── name
    ├── linux.rs
    ├── macos.rs
    ├── mod.rs
    └── windows.rs

name/${os_dependent}.rs

OS依存の処理を name/以下の linux.rs, macos.rs, windows.rsにそれぞれ書いていく。 各モジュールはOSの名前をStringで返すだけのname()関数を実装する。

name/linux.rs

pub fn name() -> String {
    "Linux".to_owned()
}

name/macos.rs

pub fn name() -> String {
    "MacOS".to_owned()
}

name/windows.rs

pub fn name() -> String {
    "Windows".to_owned()
}

name/mod.rs

各OS依存の処理を他のモジュールから扱えるようにするために name/mod.rs を記述する。

name/mod.rs

#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "linux")]
pub use self::linux::*;

#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "macos")]
pub use self::macos::*;

#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
pub use self::windows::*;

ここでのmod.rsの役割は、コンパイル対象とするモジュールの切り替えと、他のモジュールへの再エキスポートだ。

コンパイル対象とするモジュールの切り替え

これは前述のように #[cfg(target_os = "linux")] を使ってコンパイル対象のモジュールを指定する。

#[cfg(target_os = "linux")]
pub mod linux;

他のモジュールへの再エクスポート

pub useによって再エクスポートすることによって、他のモジュールからnameのOS依存モジュールを意識せずに使えるようにしている。

#[cfg(target_os = "linux")]
pub use self::linux::*;

この記述がないと、nameモジュールのOS依存処理を使う時は常にモジュール名を明確に指定しなくてはならない。 これでは以下のように他のモジュールが常にOS依存を気にしたコードを書かなくてはならず、非常によろしくない。

mod name;
let os_name = name::linux::name();

pub useすることによって、OS依存モジュール名を省略して使用することができるようになり、他のモジュールからOS依存を気にせずに使用できるようになる。

// mod.rsでpub use self::linux::*;
mod name;
let os_name = name::name();

lib.rs

最後に、今まで定義したnameモジュールを使ったlib.rsを次に記述する。

lib.rs

mod name;
pub fn greeting() {
    println!("Hello, I'm {}.", name::name());
}
// -> Hello, I'm ${YOUR_OS}.

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(target_os = "linux")]
    #[test]
    fn test_linux() {
        assert_eq!("Linux", name::name());
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn test_linux() {
        assert_eq!("MacOS", name::name());
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn test_linux() {
        assert_eq!("Windows", name::name());
    }
}

Linux上でテストを実行すると、name()は"Linux"の文字列を返してくれているのが確認できる。

$ cargo test
   Compiling hello v0.1.0 (/home/ryo/code/sandbox/rust/scraps/target_os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running target/debug/deps/hello-69d1a57a65d70c5b

running 1 test
test tests::test_linux ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests hello

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

参考

Linux kernel moduleビルドのメモ

ArchLinux(4.17.10)上でのkernel moduleビルドを試したメモ。

Linux kernelのソースコードを入手する

prepare_kernel_source.sh

#!/usr/bin/env sh
set -eu
KERNEL_VERSION=`uname -r | sed -r 's/([0-9]+\.[0-9]+\.[0-9]+).*$/\1/'`
LINUX_VERSION=linux-${KERNEL_VERSION}

if [ -e $LINUX_VERSION ]; then
    echo "$LINUX_VERSION is already exist."
    exit 1
fi

wget https://www.kernel.org/pub/linux/kernel/v4.x/${LINUX_VERSION}.tar.xz
tar -xvJf ${LINUX_VERSION}.tar.xz
ln -s ${LINUX_VERSION} kernel-source
cd ${LINUX_VERSION}
make clean && make mrproper

# prepare kernel
zcat /proc/config.gz > .config
make oldconfig
make prepare && make scripts
$ cd $HOME
$ sh prepare_kernel_source.sh

kernel moduleのソースコードMakefileを書く

hello.c

#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("MIT");

static int hello_init(void)
{
    printk(KERN_INFO "Hello, World!\n");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO "Goodbye...\n");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile

obj-m := hello.o

KERNEL_SRC_DIR=${HOME}/kernel-source
PWD:=$(shell pwd)
BUILD_DIR=$(PWD)
VERBOSE=0

all:
    make -C ${KERNEL_SRC_DIR} M=${BUILD_DIR} KBUILD_VERBOSE=${VERBOSE} modules

clean:
    make -C ${KERNEL_SRC_DIR} M=${BUILD_DIR} clean
$ make
-> hello.ko

libelf-dev でのコンパイルエラーが起こったら

コンパイルエラー

$ make
make -C /root/kernel-source M=/root/workspace/hello KBUILD_VERBOSE=0 modules
make[1]: Entering directory '/root/kernel-source'
Makefile:970: *** "Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel".  Stop.
make[1]: Leaving directory '/root/kernel-source'
Makefile:8: recipe for target 'all' failed
make: *** [all] Error 2

解決方法

$ apt-get install libelf-dev

Install/Remove kernel module

$ sudo insmod hello.ko
$ lsmod
-> Module                  Size  Used by
   hello                  16384  0
$ dmesg
-> [11869.696026] Hello, World!
$ sudo rmmod hello
$ lsmod
$ dmesg
-> [11900.796428] Goodbye...

参考

Rust: Xargoとarm-binutils無しでCortex-Mターゲットのファームウェアをビルドする

2018年4月、Rustでの組み込みプログラミング環境に2つの大きなニュースが舞い込んできた。

Xargo と arm-none-eabi-lld 無しでCortex-M向けのビルドが可能に

1つ目は、rustupのターゲットに thumbv*m-none-eabi* (Cortex-MのTarget Triple)が追加されたというニュース。

今まではrustupでは、thumbv*m-none-eabi* ターゲットがサポートされておらず、Xargo というツールを使ってlibcoreを thumbv*m-none-eabi* 用にビルドしなくてはならなかった。 しかし、nightly (nightly-2018-04-08) 以降のRustでは thumbv*m-none-eabi* が追加されたため、Xargoを使って自前でlibcoreをビルドする必要はなくなる。

nightly 2018-04-09で確認したところ、確かにターゲットリストに thumbv*m-none-eabi* が追加されていた。

$ rustup show
Default host: x86_64-unknown-linux-gnu
...
active toolchain
----------------
nightly-x86_64-unknown-linux-gnu (default)
rustc 1.27.0-nightly (4b9b70c39 2018-04-09)

$ rustup target list | pr -w 90 --columns=2
aarch64-apple-ios                            mips64el-unknown-linux-gnuabi64
aarch64-linux-android                        mipsel-unknown-linux-gnu
aarch64-unknown-fuchsia                      mipsel-unknown-linux-musl
aarch64-unknown-linux-gnu                    powerpc-unknown-linux-gnu
aarch64-unknown-linux-musl                   powerpc64-unknown-linux-gnu
arm-linux-androideabi                        powerpc64le-unknown-linux-gnu
arm-unknown-linux-gnueabi                    s390x-unknown-linux-gnu
arm-unknown-linux-gnueabihf                  sparc64-unknown-linux-gnu
arm-unknown-linux-musleabi                   sparcv9-sun-solaris
arm-unknown-linux-musleabihf                 thumbv6m-none-eabi
armv5te-unknown-linux-gnueabi                thumbv7em-none-eabi
armv7-apple-ios                              thumbv7em-none-eabihf
armv7-linux-androideabi                      thumbv7m-none-eabi
armv7-unknown-linux-gnueabihf                wasm32-unknown-emscripten
armv7-unknown-linux-musleabihf               wasm32-unknown-unknown
armv7s-apple-ios                             x86_64-apple-darwin
asmjs-unknown-emscripten                     x86_64-apple-ios
i386-apple-ios                               x86_64-linux-android
i586-pc-windows-msvc                         x86_64-pc-windows-gnu
i586-unknown-linux-gnu                       x86_64-pc-windows-msvc
i586-unknown-linux-musl                      x86_64-rumprun-netbsd
i686-apple-darwin                            x86_64-sun-solaris
i686-linux-android                           x86_64-unknown-cloudabi
i686-pc-windows-gnu                          x86_64-unknown-freebsd
i686-pc-windows-msvc                         x86_64-unknown-fuchsia
i686-unknown-freebsd                         x86_64-unknown-linux-gnu (default)
i686-unknown-linux-gnu                       x86_64-unknown-linux-gnux32
i686-unknown-linux-musl                      x86_64-unknown-linux-musl
mips-unknown-linux-gnu                       x86_64-unknown-netbsd
mips-unknown-linux-musl                      x86_64-unknown-redox
mips64-unknown-linux-gnuabi64

2つ目は、Cortex-Mバイナリ用のリンクにlldを使えるようになったというニュース。

今までは最後のリンクにはarm-binutilsのarm-none-eabi-ldを使ってリンクを行なっていたが、それをlldに置き換えられるようになった。 これはcoretex-m-rtクレートに含まれているリンカスクリプトの記述をlldに対応させたことで可能になったようだ。

この2つの変更によって、RustでのCortex-Mターゲット向けの開発のために必要なツールが減り、開発環境構築のハードルが大きく下がった印象だ。 (あとはgdbをlldbに置き換えることができれば、arm-binutilsは一切不要になる)

このニュースを報告しているJaparicさんはXargoの作者でもあり、Rustでの組み込み開発環境をずっとリードし続けている。 この方がいなかったら、今のRustはここまで組み込み分野を視野に入れたものになっていただろうか…。

nightly-2018-04-08 以降でのCortex-Mターゲットのファームウェアビルド方法

Xargo無し、arm-binutils無しでのビルド手順についてメモしておく。
動作確認には、STM32F401 Nucleo-64ボード(Cortex-M4F)を使用する。

大まかな手順は次のとおり。

  1. rustupを最新にする
  2. rustupから対象となるarm MCUthumbv*m-none-eabi* ターゲットを追加する
  3. Rustプロジェクトを作成する。
  4. .cargo/config にリンカの設定を記述する
  5. 必要なクレートを Cargo.toml追記する
  6. コードを書いてビルドする

rustupを最新にする

$ rustup update

また、ビルドにはnightlyが要求されるので、デフォルトをnightlyに変更しておく。

$ rustup default nightly

thumbv*m-none-eabi* をターゲットに追加する

今回動作確認を行うMCUはCortex-M4Fなので、rustupから thumbv7em-none-eabihf をターゲットとして追加する。

$ rustup target add thumbv7em-none-eabihf
info: downloading component 'rust-std' for 'thumbv7em-none-eabihf'
info: installing component 'rust-std' for 'thumbv7em-none-eabihf'

自分が使うCortex-M MCUがどのTarget Tripleを選べばよいかについては次のとおり。

Target Triple Target MCUs
thumbv6m-none-eabi Targets the Cortex-M0, Cortex-M0+ and Cortex-M1 processors (ARMv6-M architecture)
thumbv7em-none-eabi Targets the Cortex-M4 and Cortex-M7 processors (ARMv7E-M)
thumbv7em-none-eabihf Targets the Cortex-M4F and Cortex-M7F processors (ARMv7E-M)
thumbv7m-none-eabi Targets the Cortex-M3 processor (ARMv7-M)

Rustプロジェクトを作成する

LEDを点滅させる簡易プログラムのリポジトリを作成したため、この手順ではこちらを使う。
ryochack/Rust_NUCLEO-F401RE_Led_Blink

$ git clone https://github.com/ryochack/Rust_NUCLEO-F401RE_Led_Blink.git

一から作る場合には、 cargo new でプロジェクトを作成する。

.cargo/config にリンカの設定を記述する

.cargo/config にターゲット毎にリンカの設定を記述する。 ここではリンカにlldを指定している。

.cargo/config

[target.thumbv7em-none-eabihf]
runner = 'arm-none-eabi-gdb'
rustflags = [
  "-C", "link-arg=-Tlink.x",
  "-C", "linker=lld",
  "-Z", "linker-flavor=ld.lld",
  "-Z", "thinlto=no",
]

必要なクレートを Cargo.toml追記する

thumbv*m-none-eabi* のビルドに必要なクレートを Cargo.toml にそれぞれ追加する。

Cargo.toml

[package]
name = "nucleo-f401re_led_blink"
authors = ["ryochack"]
version = "0.1.0"

[dependencies]
cortex-m = "0.4.0"
cortex-m-rt = "0.4.0"
panic-abort = "0.1.1"
volatile-register = "0.2.0"

コードを書いてビルドする

コードが書けたら(ここではRust_NUCLEO-F401RE_Led_Blinkリポジトリのコードを使う想定)、次のコマンドでターゲットを指定してビルドする。

$ cargo build --target thumbv7em-none-eabihf

これでSTM32F401用のELFファイルが target/thumbv7em-none-eabihf/debug/nucleo-f401re_led_blink として出力される。

$ file nucleo-f401re_led_blink
nucleo-f401re_led_blink: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

このELFファイルを次の記事の内容に従ってSTM32F401 Nucleo-64ボードに書き込むことで動作が確認できる。
OpenOCD + ST-LinkでFirmware書き込み - ryochack.blog

Rustの構造体メモリレイアウト

Rustの構造体のメモリレイアウトについてのメモ。

Rustで次のような構造体を定義したときに、構造体のメモリレイアウトはどうなるか?

struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

検証時のRustのバージョンは次の通り。

stable-x86_64-unknown-linux-gnu
rustc 1.24.1 (d3ae9a9e0 2018-02-27)

TL;DR

先に結論を書く。
アトリビュート指定によって構造体のメモリレイアウトとサイズは以下のように変化する。

デフォルト

f:id:ryochack:20180323172547p:plain 構造体サイズ20Byte

repr(C)アトリビュート指定

f:id:ryochack:20180323172534p:plain 構造体サイズ24Byte

repr(packed)アトリビュート指定

f:id:ryochack:20180323172539p:plain 構造体サイズ17Byte

以下に確認の過程を残しておく。

C言語的にメモリレイアウトを予想

まずは、C言語的にRustの構造体でのメモリレイアウトを予想してみる。
各フィールドの宣言順の通りにメモリ上にデータが配置され、アライメント制約を守るために適宜パディングが入るとすると、次のようなメモリ配置になり、構造体サイズは24Byteとなるはず。

f:id:ryochack:20180323172534p:plain

確認方法

次のようなコードを書いて確認した。

use std::mem;

struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

fn main() {
    let mem = Layout {
        b1: 0,
        b2: 0,
        b3: 0,
        s1: 0,
        s2: 0,
        s3: 0,
        w1: 0,
        w2: 0,
    };

    let mem_ptr: *const Layout = &mem;
    let base = mem_ptr as usize;
    let ptr_b1: *const u8 = &mem.b1;
    let ptr_b2: *const u8 = &mem.b2;
    let ptr_b3: *const u8 = &mem.b3;
    let ptr_s1: *const u16 = &mem.s1;
    let ptr_s2: *const u16 = &mem.s2;
    let ptr_s3: *const u16 = &mem.s3;
    let ptr_w1: *const u32 = &mem.w1;
    let ptr_w2: *const u32 = &mem.w2;

    println!("Memory size = {}", mem::size_of::<Layout>());
    println!("base = 0x{:x}", base);
    println!("offset ptr_w1 = {}", ptr_w1 as usize - base);
    println!("offset ptr_w2 = {}", ptr_w2 as usize - base);
    println!("offset ptr_s1 = {}", ptr_s1 as usize - base);
    println!("offset ptr_s2 = {}", ptr_s2 as usize - base);
    println!("offset ptr_s3 = {}", ptr_s3 as usize - base);
    println!("offset ptr_b1 = {}", ptr_b1 as usize - base);
    println!("offset ptr_b2 = {}", ptr_b2 as usize - base);
    println!("offset ptr_b3 = {}", ptr_b3 as usize - base);
}

デフォルト

前述のコードを使って確認したところ、以下のような構造体のメモリレイアウトになっているのがわかった。

f:id:ryochack:20180323172547p:plain

Rustではメモリが最も節約されるように、構造体の各フィールドは各型サイズ降順に並び替えられるようだ。
そのおかげで、最初に24Byteと予想した構造体のサイズも20Byteに抑えられている。

repr(C)

前述のようなメモリレイアウトの最適化は、通常のアプリケーションを作る上ではありがたい。
しかし、組み込みソフトのドライバ開発でペリフェラルレジスタマップを構造体で定義したい場合などには、構造体のフィールドが勝手に並び替えられてしまうのは具合がよくない。
そこでRustには、構造体のメモリレイアウトの最適化を抑制する #[repr(C)] アトリビュートが用意されている。
構造体定義の頭に #[repr(C)] アトリビュートを追加してみよう。

#[repr(C)]
struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

#[repr(C)] アトリビュートを指定後のメモリレイアウトは次のようになった。 これは最初に予想したC言語的なメモリレイアウトと一致する。

f:id:ryochack:20180323172534p:plain

よって、構造体のメモリレイアウト最適化を抑制したい時には #[repr(C)] アトリビュートを指定すればいい。

repr(packed)

Rustには、#[repr(C)] アトリビュートの他にも、#[repr(packed)] アトリビュートが用意されている。
#[repr(packed)] アトリビュートは構造体のフィールド間のパディングを無くすアトリビュート指定となる。(gccなどが提供する pragma pack(1) 指定と同じ効果が得られる)

#[repr(packed)]
struct Layout {
    b1: u8,
    s1: u16,
    b2: u8,
    w1: u32,
    b3: u8,
    w2: u32,
    s2: u16,
    s3: u16,
}

#[repr(packed)] アトリビュートを指定後のメモリレイアウトは次のようになった。

f:id:ryochack:20180323172539p:plain

構造体のデータ途中のパディング、及び最後のパディングが削除されているため、構造体のサイズも最小の17Byteとなる。 ただし、このメモリレイアウトはアライメント制約を無視したものとなっているため、通信のペイロードサイズに制約がありとにかくデータを詰めて送りたい、などの場合にのみ使うのが良さそう。

※補足として、#[repr(C)] アトリビュート#[repr(packed)] アトリビュートの両方を同時に指定してもこのメモリレイアウトになる。

参考