Embedded Rust study notes

組み込み機器向けにRustを利用するための情報

標準ライブラリの構造

Rustの標準ライブラリは以下の3レベル構造で提供されている。

https://dnv825.github.io/a_jumble_of_study_notes/assets/images/3level_structure_std_library.png

coreクレートとallocクレートはstdクレートのサブセットである。

最も下層のcoreクレートは前提条件なしで利用できる。ただし、整数型やスライスのようなプリミティブ型や、アトミック操作のようなプロセッサ機能を利用する処理しか提供されていない(OSやCPUといったプラットフォームに依存しない処理しか提供されていない)。

allocクレートはBox型やVec型といったヒープメモリを利用する型を提供する。allocクレートを利用するには、メモリアロケーターの実装が必要となる。

stdクレートはファイルシステムやネットワーク、スレッドといったOS機能を提供する。println!マクロやコマンドライン引数を渡すインターフェースもstdクレートの役割である。stdクレートを利用するためにはOSが必要となる。

通常は考慮不要だが、OSが載っていない組み込み機器向けにアプリケーションを作成する際には、これらを考慮しなければならない。

unsafeブロック

FPGAボードなどを利用する際はハードウェアを直接制御することになるが、その場合「Rustコンパイラが安全でないとみなすコード」を記述する必要がある。そのために利用するのがunsafeブロックで、以下のように記述する。もちろん、記述内容の安全性は記述者であるプログラマーが保証しなければならない。

unsafe {
  // 安全でない操作を記述できる。
  // 操作の安全性はプログラマーが保障する。
}

なお、どんなコードでも書けるわけではなく、以下の5つの動作のみが許されている。

  1. ハードウェア(メモリ)の直接操作
  2. 可変なグローバル変数(static mut)へのアクセス
  3. 安全でない関数(C言語の関数など)を呼び出す
  4. unsafeなトレイトを実装する
  5. Unionへアクセスする

メモリマップドIOを操作するために、特に1番目の操作が重要となる。

必要なもの

cbindgenとは、Rustで作成したライブラリをC/C++で使うためのヘッダーファイルを生成するアプリである。

よく似た名前のツールに「bindgen」があるが、こちらのアプリの機能は逆で、C/C++で作成したライブラリをRustで使うためのインターフェースを生成する。

使い方

Rustでライブラリを作成する

まず、cargo new embedded_rustコマンドで組み込み機器向けライブラリのパッケージを作成する。パッケージ名はembedded_rustにしたが、各自で好きな名前をつければよい。

Cargo.tomlは以下のように記述する。

# Cargo.toml
[package]
name = "embedded_rust"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

[lib]
name = "rust_embedded"
crate-type = ["staticlib"] # ["staticlib"]で静的ライブラリ、["cdylb"]で動的ライブラリとなる。
path = "lib.rs"

lib.rsは以下のように記述する。

// lib.rs
// #![no_std]
#[no_mangle]

pub extern "C" fn r_return_value(arg: *mut u8) -> *mut u8{
    arg
}

準備ができ次第、cargo buildを実行する。すると、embedded_rust/target/x86_64-unknown-linux-gnu/libembedded_rust.aに静的ライブラリファイルが出力される。

cbindgenを実行し、C/C++ヘッダーファイルを生成する

基本的に https://github.com/eqrion/cbindgen#quick-start に従えばよい。まずは以下のコマンドでcbindgenをインストールする。

cargo install --force cbindgen

次に、https://github.com/eqrion/cbindgen/blob/master/template.toml からcbindgen.tomlのテンプレートをダウンロードし、embedded_rust/cbindgen.tomlへ配置する。

続いてcbindgen.tomlを開き、languate = "C++"と指定している箇所をlanguage = "C"に変更する。こうすることで、C言語のヘッダーファイルを出力するようになる。

# cbindgen.toml
language = "C"

準備完了後、以下のコマンドを実行する。すると、embedded_rust/embedded_rust.hが生成される。

cbindgen --config cbindgen.toml --crate embedded_rust --output embedded_rust.h

生成されたヘッダーファイルの中身は以下のようになっていた。

// embedded_rust.h
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>


uint8_t *r_return_value(uint8_t *arg);

C言語からRustライブラリを呼び出す

ライブラリとヘッダーファイルを生成したので、ライブラリを呼び出すC言語のアプリを作成する。まず、以下のコマンドでプロジェクトフォルダを作成し、生成したライブラリとヘッダーファイルを移動する。

# カレントディレクトリはembedded_rustディレクトリの想定。
mkdir c_bin_sample
cp ./target/x86_64-unknown-linux-gnu/debug/embedded_rust.a c_bin_sample/
cp ./embedded_rust.h c_bin_sample/

続いてmain.cMakefileを作成する。main.cは以下のように記述する。

// main.c
#include <stdio.h>
#include "embedded_rust.h"

int main(int argv, char* argc[])
{
    printf("%s\n", r_return_value("Hello cbindgen world!"));
    return 0;
}

Makefileは以下のように記述する($(CC)の行頭はタブで記述すること。テキストフォーマッターの影響でスペースに置換されてしまった)。

# Makefile
CC = gcc
TARGET = call_rust_library

$(TARGET): main.c
 $(CC) -o $(TARGET) main.c embedded_rust.a

出来上がったC言語ソースをmakeして実行すると、以下のようにRustライブラリが動作する。

make
./call_rust_library
  → Hello cbindgen world!

extern "C"#[no_mangle]アトリビュートの指定

Rustで関数をライブラリ化してC言語から呼び出す場合、Rustの記述は以下のようにする必要がある。

#[no_mangle]
pub extern "C" fn r_10_times_value(buf: *mut i32) -> *mut i32 {
    unsafe {
        // 以下のどちらでもOK。
        // *buf = *buf * 10;
        buf.write(*buf * 10);
    }
    buf
}

extern "C"はABIの指定、つまりC言語で生成したバイナリと互換性を持たせますという意味で、#[no_mangle]アトリビュートはコンパイル時に関数名を変更しない・情報を追加しないという意味になる。これらのオプションを記述しなければ、C言語から関数を呼び出すことはできない。

参照:https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#他の言語からrustの関数を呼び出す

他の言語からRustの関数を呼び出す

また、externを使用して他の言語にRustの関数を呼ばせるインターフェイスを生成することもできます。 externブロックの代わりに、externキーワードを追加し、fnキーワードの直前に使用するABIを指定します。 さらに、#[no_mangle]注釈を追加してRustコンパイラに関数名をマングルしないように指示する必要もあります。 マングルとは、コンパイラが関数に与えた名前を他のコンパイル過程の情報をより多く含むけれども、人間に読みにくい異なる名前にすることです。 全ての言語のコンパイラは、少々異なる方法でマングルを行うので、Rustの関数が他の言語で名前付けできるように、 Rustコンパイラの名前マングルをオフにしなければならないのです。

C言語の制約

文字列リテラルはメモリ上の書き込み禁止領域に配置される

Rustで文字列を生成すると文字列リテラルとなり書き込み禁止領域に配置されるが、C言語でchar*型変数に文字列をセットした場合も同様に文字列リテラルとなり、書き込み禁止領域に配置される。

そのため以下のソースを実行するとSegmentation faultが発生してしまう。

#include <stdio.h>
#include <string.h>

void c_plus_one_char_literal_value(char*, size_t);

int main(void) {
    //--------------------------------------------------------------------------
    // char*型の値を書き換える。
    // 書き込み禁止領域に文字列リテラルが配置されるため、Segmentation faultが発生する。
    //--------------------------------------------------------------------------
    char* char_literal = "Hello Rust FFI world!";
    unsigned int char_literal_length = strlen(char_literal);

    printf("char* char_literal, + 1.:\n");

    printf(" %-7s", "(C)");
    printf(" %s ", char_literal);
    c_plus_one_char_literal_value(char_literal, char_literal_length);
    printf("=> %s\n", char_literal);
}

// 引数に渡された文字列リテラルの各要素の値を+1する。
void c_plus_one_char_literal_value(char* character_literal, size_t length) {
    for (size_t i = 0; i < length; i++) {
        character_literal[i] += 1;
    }
}

以下のように配列として宣言すれば、書き込み可能な領域に文字の配列が確保される。C言語側でバッファを確保する際、うっかり文字列リテラルのような方式にしないよう注意が必要。

#include <stdio.h>
#include <string.h>

void c_plus_one_char_array_value(char*, size_t);

int main(void) {
    //-----------------------------------------------------------------
    // charの配列の値を書き換える。
    // 書き込み可能な領域に文字の配列が配置されるため、配列の値を変更できる。
    //-----------------------------------------------------------------
    char char_array[] = "Hello Rust FFI world!";
    unsigned int char_array_length = strlen(char_array);

    printf("char char_array[], + 1.:\n");

    printf(" %-7s", "(C)");
    printf(" %s ", char_array);
    c_plus_one_char_array_value(char_array, char_array_length);
    printf("=> %s\n", char_array);
}

// 引数に渡された文字列リテラルの各要素の値を+1する。
void c_plus_one_char_array_value(char* character_literal, size_t length) {
    for (size_t i = 0; i < length; i++) {
        character_literal[i] += 1;
    }
}

gccのオプション

WSL2のUbuntu 20.04.5 LTSでC言語をコンパイルする場合、以下のコンパイルオプションが必要となる。

# 以下のようにヘッダーファイル、ライブラリをソースコードと同じフォルダに配置する場合。
#
#   src
#     header_sample.h
#     libsample.a
#     main.c 
gcc -o <ターゲット名> <ソースコード名> <ライブラリ名> -pthread -Wl,--no-as-needed -ldl

# 以下のようにヘッダーファイル、ライブラリをソースコードと異なるフォルダに配置する場合。
#
#   src
#     include
#       header_sample.h
#     lib
#       libsample.a
#     main.c
gcc -o <ターゲット名> -I <インクルードパス> <ソースコード名> -pthread -Wl,--no-as-needed -ldl -L <ライブラリパス> -l <ライブラリファイル名> 

ソースコード名の後ろにライブラリ名を指定するのがポイント。それぞれのコンパイルオプションの意味は、

  • -pthreadはPOSIX スレッド ライブラリとリンクするということ。
  • -Wlはコンマ以降の指定をリンカへ渡す指定。
    • 例えば-Wl,-Map,output.mapを指定すると、-Map output.mapをリンカーに渡す。
    • GNU リンカを使用する場合、-Wl,-Map=output.mapでも同じ効果が得られる。
    • --no-as-neededは「アプリケーションで実際に使われていない共用ライブラリであってもリンクする」という意味。
      • デフォルト動作は--as-neededで、「アプリケーションに実際に使われている共用ライブラリのみリンクする」という意味。
      • Ubuntu 11.10から--as-neededがデフォルト仕様になった。
  • -Lはライブラリファイルを配置したパスをリンカへ渡す指定。
    • -Lコマンドで指定したパスは、-lコマンドで指定したライブラリファイルの検索対象になる。
  • -lはライブラリファイル名をリンカへ渡す指定。
    • 例えば-lmlibm.alibm.soを指定した扱いになる(”lib”が勝手に付与され、拡張子は無視される)。
    • そのため、-ldllibdl.alibdl.soを指定したことになる。
    • libdl.soをリンクしていることは確認できたが、何に使っているのかは不明。
      • 動的ライブラリのシンボル名を解決するらしいが…

実際にMakefileにすると以下のようになる。なお、フォーマッターが冒頭のタブをスペースに置き換えているかもしれない。コマンド部分の先頭はタブで始める必要があるので、コピペする際は注意すること(例えば-@cpの前の文字はタブ文字が正しい)。

CC = gcc 
TARGET = call_rust_library 
SRC = main.c
INCLUDE_PATH = include
LIB_PATH = lib
LIB_FILES = rust_embedded
LDFLAGS = -pthread -Wl,--no-as-needed -ldl -L $(LIB_PATH) -l $(LIB_FILES) 

$(TARGET): main.c
 -@cp -u ../target/debug/embedded_rust.a ./lib/
 -@cp -u ../embedded_rust.h ./include/
 $(CC) -o $(TARGET) -I $(INCLUDE_PATH) $(SRC) $(LDFLAGS)

.PHONY: clean
clean:
 -@rm -f $(TARGET) $(INCLUDE_PATH)/* $(LIB_PATH)/*

実行ファイルに対してlddコマンドを指定することで、どの共有ライブラリ(Shared Object = 動的ライブラリ)を参照しているか調べることができる。

wsluser@PC-C0204-2207-1:/mnt/c/workspace/embedded_rust$ ldd c_bin_sample/call_rust_library 
        linux-vdso.so.1 (0x00007ffd6bf68000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f84af5bf000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f84af5a4000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f84af581000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f84af38f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f84af717000)

(TODO)具体例

Linuxのデバイス制御方法について

メモリーマップ

そもそもアプリケーションとは「CPUがメモリーの情報を読み書きし演算を行うこと」を言います。この時、「メモリー上のある番地の読み書き」を「接続した機器の操作」と対応させたものをメモリーマップと呼びます(厳密にいうと、内臓機器・外部機器・ユーザーの作成したコードなど色々なものが対応付けられます)。

ハードウェア制御に欠かせないのが「レジスタ」と呼ばれる特殊なメモリーで、プログラムから「レジスタ」の読み書きを行うことでハードウェアを制御することが可能です。

なお、ここで言う「レジスタ」は、CPU内のレジスタとは異なる概念です。

どのアドレスがレジスタなのかを示した情報をメモリーマップと呼びます。メモリーマップは製品カタログや説明書に書いてあります。

linuxでは、現在動いているプロセスのプロセス制御テーブルのマップを/procに配置された疑似ディレクトリに配置しています。ps -efを実行してPIDを確認し、/proc/{PID}/mapsの中にあるファイルを開くとメモリーマップを確認できます。

リンカスクリプト

C言語やC++で実行可能ファイルを作成する場合、まずコンパイラがオブジェクトファイルを生成し、次にリンカがオブジェクトファイルを取りまとめて実行可能ファイルを生成します。

リンカが実行可能ファイルを生成する際、メモリ空間上へどのように関数や変数といったシンボルを配置するかを決定します。その配置位置をリンカに指示するためのファイルがリンカスクリプトです。汎用OS向けにアプリケーションを作成する場合、OS側が仮想アドレスを提供してくれるので、デフォルトのリンカスクリプトが利用されます。しかし、組み込み機器向けの開発を行う場合は自分でリンカスクリプトを書かなければいけないことがあります。

GNUだとldがリンカです。以下のアドレスはldの説明書です。

https://sourceware.org/binutils/docs-2.35/ld/index.html

デバイスツリー

デバイスツリーとはハードウェア情報を記述したデータ構造体のことです。ハードウェアの差分をデバイスツリーに記述することで、ハードウェア変更時にLinuxカーネルを変更しなくて済むようになります。

ただし、デバイスツリーに記述できるのはハードウェアの構成情報だけで、処理を記述することはできません。

以下の3つの用語はデバイスツリーに関する言葉です。dtcのインストールはsudo apt install device-tree-compilerで行えます。

略語 意味
dtb device tree blob
dts device tree source
dtc device tree compiler

デバイスドライバ

参照元:はじめて学ぶデバイスドライバ開発。組み込みLinuxによるハードウェア制御の仕組みを学ぶ

デバイスドライバとは

デバイスドライバは、制御対象のデバイスを適切にコントロールし、ハードウェアが提供する機能を運用。アプリケーションをはじめとする他のプログラムに対して、機能を実現するために不可欠なAPI内の実装を提供するソフトウェアです。

デバイスドライバを経由してGPIOデバイスを操作する

Linuxにおけるデバイスドライバは、/sys/class/gpio/にファイルとして保持されています。このファイルを読み書きすることでデバイスを操作することができます。

https://monozukuri-c.com/mbase-hardcontrol/ https://uquest.tktk.co.jp/embedded/learning/lecture11.html https://uquest.tktk.co.jp/embedded/learning/lecture16.html https://brain.cc.kogakuin.ac.jp/~kanamaru/lecture/MP/final/ https://brain.cc.kogakuin.ac.jp/~kanamaru/lecture/MP/final/part06/node8.html

テキスト領域(プログラム領域)  Low Address
静的領域
ヒープ領域
  |
  ∨


  ∧
  |
スタック領域                    High Address

論理的なメモリ上の模式図

ここでいう「論理的」とは、ハードウェア上の配置ではなく、OSによって提供された仮想的なメモリの配置を表現していることを意味する。 静的領域とヒープ領域をあわせて「データ領域」と呼ぶこともある。

テキスト領域:機械語に翻訳されたプログラムが格納される. この機械語の命令が 1 行づつ実行されることでプログラムが動く。 静的領域:グローバル変数などの静的変数が置かれる。 ヒープ領域:メモリの動的管理 (C 言語の malloc 関数や C++ の new 演算子でメモリを確保すること) で用いられる。 スタック領域:今回の演習で扱ったように CPU のレジスタを一時的に退避させたり、また C 言語の自動変数 (多くのローカル変数) が置かれる。

BSS: Block Starting Symbol

https://jhalfmoon.com/dbc/2019/10/11/ぐだぐだ低レベルプログラミング12-オブジェクト/

.text: プログラム本体、機械語命令コードを収める領域。 .data: 初期値が与えられた変数などを収める領域。 .bss: 初期値のない変数などを収める領域。 .rodata: 定数を収める領域。

https://e-words.jp/w/フェッチ.html

マイクロプロセッサ(CPU/MPU)では、命令を実行する最初の段階で、命令コード(インストラクション)をメインメモリ(またはキャッシュメモリ)から読み出し、プロセッサ内部のレジスタに転送する動作のことをフェッチという。フェッチされた命令デコード(解析)されて実行に移される。フェッチにかかる時間を「フェッチサイクル」(fetch cycle)あるいは「命令サイクル」(instruction cycle)という。

https://msyksphinz.hatenablog.com/entry/2017/02/09/010728

VMA: Virtual Memory Address LMA: Load Memory Address

参考資料

https://doc.rust-lang.org/nomicon/ffi.html https://dev.to/kgrech/7-ways-to-pass-a-string-between-rust-and-c-4ieb https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html https://teratail.com/questions/360046 http://www.nct9.ne.jp/m_hiroi/linux/rustabc04.html https://stackoverflow.com/questions/28050461/how-can-i-index-c-arrays-in-rust https://stackoverflow.com/questions/29182843/pass-a-c-array-to-a-rust-function https://dev.to/kgrech/7-ways-to-pass-a-string-between-rust-and-c-4ieb https://michael-f-bryan.github.io/rust-ffi-guide/ https://rust-unofficial.github.io/patterns/idioms/ffi/passing-strings.html https://qiita.com/HAMADA_Hiroshi/items/ed9305e377dc7e10fbe5 https://wa3.i-3-i.info/word13813.html http://os.inf.tu-dresden.de/pipermail/l4-hackers/2011/005078.html https://nxmnpg.lemoda.net/ja/3/dlsym

https://stackoverflow.com/questions/1662909/undefined-reference-to-pthread-create-in-linux https://github.com/rust-lang/rust/issues/47714 https://stackoverflow.com/questions/20369672/undefined-reference-to-dlsym https://www.gnu.org/software/make/manual/make.html https://zudoh.com/linux/light-about-makefile

  1. -, Cと少しのRust, The Embedded Rust Book(日本語版), -, https://tomoyuki-nakabayashi.github.io/book/interoperability/rust-with-c.html
  2. eqrion and 100 Contributors, cbindgen, GitHub, 2022/11/14, https://github.com/eqrion/cbindgen

組み込みRustの資料

  1. tomoyuki-nakabayashi, The Embedded Rust Book, -, -, https://tomoyuki-nakabayashi.github.io/book/intro/index.html
  2. getditto, safer_ffi User Guide, -, -, https://getditto.github.io/safer_ffi/introduction/_.html
  3. michael-f-bryan, Rust FFI Guide, -, -, https://michael-f-bryan.github.io/rust-ffi-guide/overview.html
  4. Will Crichton, Memory Safety in Rust: A Case Study with C, &notepad, 2018/02/02, https://willcrichton.net/notes/rust-memory-safety/
  5. -, Rust Design Patterns, -, -, https://rust-unofficial.github.io/patterns/intro.html
  6. dbrgn, Calling Rust from C and Java, -, 2017/10/31, https://speakerdeck.com/dbrgn/calling-rust-from-c-and-java?slide=20

results matching ""

    No results matching ""