SOLID未分類 SOLID for Raspberry Pi 4 (連載23)

SOLID for Raspberry Pi 4 (連載23)

(2023/4/17)

23.C言語からRustの関数をコールする方法

前回、solidクレート側で、SOLID OSの持つC/C++関数を、Rustからコールする方法について見てみました。

今回はその逆で、C/C++言語からRustの関数をコールする方法について考えていきたいと思います。
個人的にはこちらの方がよくあるケースではないかなぁと思うのですが、、、どうでしょうか。一部分だけRust化するイメージです。

では、行ってみましょう!

 

1.ワークスペース作成

過去記事を参考にして、ワークスペースを作ります。
https://note.com/kmc715/n/n3b503a948df5?magazine_key=m76fc246deaa1

「1.1 Rustのライブラリをリンクする元のワークスペースを作る」
と、
「1.2 RustのLibraryプロジェクトをワークスペースに追加する」
が参考になります。

※メインプロジェクトのリンカ設定で、 追加のライブラリファイル にライブラリ名を指定する部分について一点注意事項です。
Release構成だけでなく、Debug構成に対しての設定部分にもライブラリ名を入力することを忘れないようにしないといけません。
これを忘れると、Debugビルドをしている際に「リンクでエラーになる」としばらく悩むことになります。

ワークスペースは以下のように作成できました。

この段階で、C++側とRust側にはそれぞれ以下のコードが生成されています。

!!!

ここで気が付いたのですが、これってRust側はもう、C/C++言語側からコールされる準備が整っている?

 

2.no_mangle

 

Rustでは、同じシンボル名があっても重複しないよう、ビルド時にシンボル名を変更します。その作業をmanglingと言います。

一方、呼び出しをしたいC/C++言語側からすると、シンボル名はそのままにして欲しいところです。

 

このため、C/C++言語側から呼び出されるRust側の関数には、

#[no_mangle]

を付け、manglingしないでね、とお願いします。

 

このあたりの説明が、SOLIDの公式資料にありました。

https://solid.kmckk.com/SOLID/doc/3.1.0/solid_rust/tutorial/add-task.html

※この説明は、Rustのタスクを追加するための資料ですが、#[no_mangle]についても補足されています。

 

という事は、もう先程のワークスペースの段階で、C/C++言語側からコールされるに違いない!

やってみます。

 

C++側とRust側には以下のコードを書きました。

UART出力は以下になりました。

println!("Hello, world from Rust lib!");

が実行されていることがわかります。

無事、C/C++言語側からRustの関数がコールされました!

 

 

3.引数を渡す(C/C++からRust)

関数コールなので、引数を渡せるはずです。
Rustの引数のデータ型と、C/C++の引数のデータ型って、どのように変換されるのでしょうか。

Rustのデータ型:

・整数型:

Length   Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

・その他:
浮動小数点型: f32, f64
ブーリアン型: bool
文字型: char

補足:
charについて、両者全く違う型です。
・C/C++ のchar :1バイト(8ビット)の整数型
・Rust のchar  :Unicode scalar valueを表す4バイトの型
C/C++ のcharは、Rustではstd::ffi::c_char (= i8) に対応します。
(以下、実験なので、C/C++ のcharとRustのcharを同じ位置に配置してみます)

参考公式URL:
https://doc.rust-lang.org/stable/book/ch03-02-data-types.html

 

ではこの中から抽出して、以下のデータ型をRust関数の引数として指定してみます。
u8, u32, u64, usize, f64, bool, char

C/C++側のコードは以下です。

#include 

extern "C" void func1(uint8_t, uint32_t, uint64_t, unsigned int, double, bool, char);
extern "C" void slo_main()
{
	uint8_t u8_val=0xa5;
	uint32_t u32_val = 0xF0F0F0F0;
	uint64_t u64_val = 0x2222222211111111l;
	unsigned int uint_val = 0x123456789abcdef0;
	double f64_val = 1.2345;
	bool bool_val=true;
	char char_val = 'A';

	SOLID_LOG_printf("Hello, world from C\n");
	func1(u8_val, u32_val, u64_val, uint_val, f64_val, bool_val, char_val);
	return;
}

 

Rust側のコードは以下です。

#[no_mangle]
pub extern "C" fn func1(u8_val_rust:u8, u32_val_rust: u32, u64_val_rust: u64, uint_val_rust: usize, f64_val_rust: f64, bool_val_rust : bool, char_val_rust : char) {

	println!("Hello, world from Rust lib!");
	println!("u8={}, u64={}, u32={}, usize={}, f64={}, bool={}, char={}"
	  ,u8_val_rust, u32_val_rust, u64_val_rust, uint_val_rust, f64_val_rust, bool_val_rust, char_val_rust);
}

引数で渡されてきたデータをただ表示するだけの、簡単なプログラムです。

 

まず、C/C++側でRust関数を呼び出すところでブレークして、変数をWatchしてみます。

実際のメモリにはどう配置されているか、スタックポインタ付近を見てみました。

データ境界補正付きで、うまく入っています。

ちなみに、1.2345(double)=0x3FF3C083126E978Dになります。

unsigned intはuint32_tと同じなので、32ビットですね。

(ここでは実験的に0x123456789abcdef0と64ビットで代入してみました。実際は下位の32ビットとなります。)

 

次に、Rust関数の中に入ってみます。

Rust側のusizeはCPU依存です。

このCPUはCortex-A72で64ビットなので、usizeもuint64ビットになっていますね。

C/C++側のuint_valとはビット数違いますが、まぁ実験なのでこのままやっていきましょう。

とりあえずは、意図した通りに引数がわたっていました。

ここで疑問。
データの並びがC/C++とRust関数側で違っていた場合はどうなるのでしょう?

ちょっとこれから、Rust関数側の引数を間違える実験をやってみます。

 

まず、
「あ!Rust側のu32_valのサイズ間違った!」
の場合を試してみます。

Rust側のu32_valのサイズをu16にしてみますね。

あら、ずれていない。
u32をu16にしたので、その次のデータとのデータ境界を考慮された結果、データのずれは発生せず、という事でしょうか。

では次、
「あ!Rust側のf64_val入れ忘れた!」
をやってみます。

えええーー!、なぜ???
なぜ、ずれていないのか!

想像では、
・C/C++側のfunc1()関数:uint_valの次f64_val 、次bool_val、次char_val
・Rust側のfunc1()関数:uint_valの次bool_val、次char_val
なので、
bool_val = もともとf64_valがあったデータの先頭1バイト -> 0x3f (trueかな?)
char_val = もともとf64_valがあったデータの二つ目1バイト -> 0xf3
になるのではないか、と思っていました。

では、次。
「あ!Rust側のu32_valとbool_valの位置が逆!」
をやってみます。

入ってるデータの値が逆になったようです。

u32_valが0x1、すなわち、もともとのbool_valの値になりました。
そしてbool_valはfalseになったので、多分u32_valの最初の1バイト0x0fを見ているのかなと思います。

入っているデータのビット数について見てみましょう。
u32_valは0x01 なので、これは1バイトしか見てないですね。

やはり、何らかの手段でC/C++側から、引数の順序におけるサイズ情報がRust側に伝えられているのでしょうか。

1つ目:オフセット0から1バイト(u8_val)
2つ目:オフセット4から4バイト(u32_val)
3つ目:オフセット8から8バイト(u64_val)
4つ目:オフセット16から4バイト(usize_val: C/C++側は32ビットです)
5つ目:オフセット24から8バイト(f64_val)
6つ目:オフセット32から1バイト(bool_val)
7つ目:オフセット33から1バイト(char_val)

そういえば先週、RustからC/C++関数をコールする際の、構造体の引き渡しの際にも同じような処理がありましたね。

先週の記事の「4.具体例で追ってみる(2) – 構造体」の辺りです。

/// Layout check of `SOLID_INTC_HANDLER`
const _: () = {
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET0 == offset_of!(SOLID_INTC_HANDLER, intno));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET1 == offset_of!(SOLID_INTC_HANDLER, priority));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET2 == offset_of!(SOLID_INTC_HANDLER, config));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET3 == offset_of!(SOLID_INTC_HANDLER, func));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_OFFSET4 == offset_of!(SOLID_INTC_HANDLER, param));
    assert!(_SOLID_RS_SOLID_INTC_HANDLER_SIZE ==  size_of::<SOLID_INTC_HANDLER>());
};

「サイズとオフセットを検証しています」と書いている辺りです。

 

うーん、だとしても、Rust側関数の引数にf64_valをつけ忘れた場合に、その次の引数からずれなかったのが説明つきません。

しつこいようですが、あと一つだけ。
「あ!Rust側のbool_val入れ忘れた!」

その次のchar_valも、この入れ忘れたbool_valも、同じ1バイトなので、もしかするとずれるかも。。。

あ!

ずれた!

やっぱり、その次の引数がずれる場合もあるんだ!

という事で、C/C++からRust関数呼ぶ際には、当然のことですが引数の順番や型のサイズには十分注意しましょう、という実験でした。

C/C++同士、Rust同士の関数コールであれば、ビルドでエラーになりますが、言語間をまたがっての関数コールなので、当然そこはコーディングする人が気をつけなければいけないですね。

 

4.戻り値(C/C++からRust)

引数の順番で盛り上がってしまいました。

次は、戻り値の書き方について見てみます。

といっても、多分Rust側はRustのお作法にのっとり戻り値を返し、C/C++側はその関数が返してくる値を普通に読めばいいのではないかと思います。

やってみます。

C/C++側のコードは以下です。

#include 

extern "C" int32_t func1(uint8_t, uint32_t, uint64_t, unsigned int, double, bool, char);
extern "C" void slo_main()
{
	uint8_t u8_val=0xa5;
	uint32_t u32_val = 0xF0F0F0F0;
	uint64_t u64_val = 0x2222222211111111l;
	unsigned int uint_val = 0x123456789abcdef0;
	double f64_val = 1.2345;
	bool bool_val=true;
	char char_val = 'A';

	SOLID_LOG_printf("Hello, world from C\n");
	int32_t rtn = func1(u8_val, u32_val, u64_val, uint_val, f64_val, bool_val, char_val);
	SOLID_LOG_printf("Return from Rust = %d\n", rtn);
	return;
}

Rust側のコードは以下です。

#[no_mangle]
pub extern "C" fn func1(u8_val_rust:u8, u32_val_rust: u32, u64_val_rust: u64, uint_val_rust: usize, f64_val_rust: f64,
		bool_val_rust : bool, char_val_rust : char) -> i32 {

	println!("Hello, world from Rust lib!");
	println!("u8={}, u64={}, u32={}, usize={}, f64={}, bool={}, char={}"
	  ,u8_val_rust, u32_val_rust, u64_val_rust, uint_val_rust, f64_val_rust, bool_val_rust, char_val_rust);

	return 567; 
}

 

無事、動きました。

 

以上、先週に引き続き、今週はRustとC/C++の間の関数コールについて見てきました。

連載ももう第23回となり、筆者としても半年以上Rustの勉強をしてきていることになります。これで自信をもって書けるか?と言われると、正直変数の借用・所有権あたりはまだまだです。お作法はだんだんとわかるようになってきたので、これから慣れていくフェーズが必要なのかな、という風に思います。

今週はここまで。
来週は、C/C++とRustの間をもっと深堀してみようと思います。