RustでZephyrのアプリケーションを書く

github.com

最近、ZephyrというRTOSで遊んでいます。 新しい環境が手に入ると、Rustと結びつけたくなりますよね!

ということで、ZephyrのアプリケーションをRustで書いてみます。 ズルしましたけど、hello worldは動きました。

知見として、no_stdだけど完全なfree standing環境じゃない場合、一工夫必要そうな感触でした。

基本戦略

Zephyrをqemu_cortex_m3ターゲットでビルドして、hello worldをします。 ベースとするのは、Zephyrのサンプルアプリです。

|> zephyr/samples/hello_worldを使います。中身は下のような感じです。

main.c

#include <zephyr.h>
#include <misc/printk.h>

void main(void)
{
    printk("Hello World! %s\n", CONFIG_BOARD);
}

Zephyrのプロジェクトでは、アプリケーションコードはライブラリ(libapp.a)としてビルドされ、最後にZephyr本体とリンクされます。。

|> <build>/app/libapp.a

Rustのプロジェクトをライブラリ(libapp.a)として作って、C言語で書かれたアプリケーションの代わりにリンクしてあげれば良いはずです。

Let's Hello World...できない!

ライブラリとしてプロジェクトを新規作成します。

cargo new --lib hello

プロジェクトの設定をします。

$ tail -n 3 Cargo.toml
[lib]
name = "app"
crate-type = ["staticlib"]

Cortex-M3をデフォルトのビルドターゲットにします。

$ cat .cargo/config
[build]
target = "thumbv7m-none-eabi"

|> src/lib.rs

#![no_std]

#[no_mangle]
pub extern "C" fn main() -> ! {
    loop{}
}

use core::panic::PanicInfo;
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) -> ! {
    loop{}
}

とりあえず無限ループでQEMUが起動することを確認します。Zephyrでは、アプリケーションのエントリポイントはmainという名前の関数なので、Rust側もそれに合わせます。

では、Rust側をビルドしましょう。

$ cargo build
$ ls target/thumbv7m-none-eabi/debug/libapp.a
target/thumbv7m-none-eabi/debug/libapp.a

無事、libapp.aができています。

Zephyr側も一度ビルドします。 そして、Zephyrのlibapp.aを、Rustで作ったlibapp.aに置き換えます。 これで、再度Zephyrをビルドすれば、Zephyr本体とRustで書いたライブラリがリンクされてめでたしめでたし、のはずです!

ninja
arm-none-eabi/bin/ld: zephyr/lib/libc/minimal/liblib__libc__minimal.a(string.c.obj): in function `memcmp':
zephyr/lib/libc/minimal/source/string/string.c:179: multiple definition of `memcmp'; app/libapp.a(compiler_builtins-f49b1fd2880b372d.compiler_builtins.9n9954vw-cgu.0.rcgu.o):/rustc/9fda7c2237db910e41d6a712e9a2139b352e558b//src/rustc/compiler_builtins_shim/../../libcompiler_builtins/src/mem.rs:55: first defined here
...
# 以下、memmove, memcpy, memsetに関する同様のエラー
collect2: error: ld returned 1 exit status
%
ninja: build stopped: subcommand failed.

ということで、memcmp, memmove, memcpy, memsetの多重定義エラーが発生しました。忍者もしょんぼりです。

リンクエラーの調査

ZephyrはしっかりとしたOSなので、ユーザアプリケーションを書くために、最低限のlibcを提供しています。この中には、memcmpやmemmoveが含まれています。

一方、no_stdなRustでは、core libraryの機能を提供するために、compiler_builtinsというクレートを利用しています。 この中に、問題になっているmemcmpやmemmoveといった関数が、C言語のインタフェースで定義されています。

下記からソースコードを一部抜粋します。

compiler-builtins/mem.rs at master · rust-lang-nursery/compiler-builtins · GitHub

#[cfg_attr(all(feature = "mem", not(feature = "mangled-names")), no_mangle)]
pub unsafe extern "C" fn memcpy(dest: *mut u8,
                                src: *const u8,
                                n: usize)
                                -> *mut u8 {
    let mut i = 0;
    while i < n {
        *dest.offset(i as isize) = *src.offset(i as isize);
        i += 1;
    }
    dest
}

これらのシンボルがリンク時に衝突するため、リンクエラーになっていまいます。

応急処置

ということで、応急処置をして無理矢理動かします。 具体的には、Rust側のmemcmpやmemmoveのシンボルをweakに書き換えます。

シンボルをweakにすれば、通常の定義があれば、weakな方は無視されます。

arm-none-eabi-objcopy --weaken-symbol=memmove --weaken-symbol=memcpy --weaken-symbol=memset --weaken-symbol=memcmp libapp.a

これでRust側のlibapp.a内にある邪魔なmemcmpなどのシンボルはweakになります。

$ ninja run
[3/7] Linking C executable zephyr/zephyr_prebuilt.elf
...
[6/7] Linking C executable zephyr/zephyr.elf
...
[7/7] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
***** Booting Zephyr OS zephyr-v1.13.0-3321-g7f956a9 *****

リンクが通って、Zephyrが起動しました!

無理矢理Hello World

Zephyrのprintk()は次のように宣言されています。

void printk(const char *fmt, ...);

C言語のフォーマット形式をRustから呼び出すのは面倒くさそうなので、ラッパー関数を作ります。

void puts_c(const char *str) {
    printk("%s", str);
}

面倒くさいので、Zephyr内(zephyr/misc/printk.c)に直接突っ込んでいます。良い子は真似しちゃダメだぞ★

後はRust側からこのputs_c()を呼び出すだけです。

extern "C" {
    fn puts_c(c_str: *const u8);
}

#[no_mangle]
pub extern "C" fn main() -> ! {
    const HELLO: &[u8] = b"Hello from Rust.\0";
    unsafe { puts_c(HELLO.as_ptr()) };

    loop{}
}

改めて、Rustのプロジェクトをビルドして、libapp.aを置き換えて、シンボルをweakにして、

$ ninja run
***** Booting Zephyr OS zephyr-v1.13.0-3321-g7f956a9 *****
Hello from Rust.

いぇーい!