TOPPERS APIを使用する (方法1)
C/C++コードとの相互運用を実現する仕組みをFFIと言います。本セクションでは手動でFFI関数・型宣言を行うことにより、TOPPERSカーネルのAPIを直接利用する方法を説明します。
使用したいカーネルAPIを選びます。ここでは例として以下のAPIを選ぶことにします。
typedef signed int int_t; /* 自然なサイズの符号付き整数 */ typedef unsigned int uint_t; /* 自然なサイズの符号無し整数 */ typedef int_t ER; /* エラーコード */ typedef int_t ID; /* オブジェクトのID番号 */ typedef uint_t ATR; /* オブジェクトの属性 */ typedef int_t ER_ID; /* エラーコードまたはID番号 */ typedef struct t_csem { ATR sematr; /* セマフォ属性 */ uint_t isemcnt; /* セマフォの初期資源数 */ uint_t maxsem; /* セマフォの最大資源数 */ } T_CSEM; typedef struct t_rsem { ID wtskid; /* セマフォの待ち行列の先頭のタスクのID番号 */ uint_t semcnt; /* セマフォの現在の資源数 */ } T_RSEM; extern ER_ID acre_sem(const T_CSEM *pk_csem) throw(); extern ER sig_sem(ID semid) throw(); extern ER wai_sem(ID semid) throw(); extern ER ref_sem(ID semid, T_RSEM *pk_rsem) throw();
型定義をRustで記述します。
#[allow(non_camel_case_types)] mod ffi { pub type int_t = i32; pub type uint_t = u32; pub type ER = int_t; pub type ID = int_t; pub type ATR = uint_t; pub type ER_ID = int_t; #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct T_CSEM { pub sematr: ATR, pub isemcnt: uint_t, pub maxsem: uint_t, } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct T_RSEM { pub wtskid: ID, pub semcnt: uint_t, } }
Note
mod ffi
はffi
という名前の子モジュールを定義します。通常はソースファイル全体がモジュールに対応しますが、このようにソースファイル内に子モジュールの内容を埋め込むこともできます。lib.rs
で記述した場合、この子モジュールの完全パスはcrate::ffi
となります。pub
指定によりffi
モジュールの外からでもアクセス可能にします。#[repr(C)]
[Rustリファレンス] を指定することで構造体レイアウトがC言語と互換になるようにします。#[derive(Debug, Copy, Clone)]
[The Book] は型に各種トレイトを自動的に実装します。Debug
は値のデバッグ用文字列表現を生成する共通のインタフェースを表します。Copy
はユーザ定義のコードを必要とせずに値を複製できることを表します。Clone
は値を複製するため共通のインタフェースを表します。derive
で生成される実装は個別のフィールドに対して再帰的にClone::clone()
メソッドを呼び出します。ここではderive
を使用していますが、derive
を使わずに手動で実装するのも参照カウントを行うスマートポインタなどの実装に有用です。
#[allow(non_camel_case_types)]
[Rustリファレンス] はRustの命名規則に従わない型名に対する警告を抑止します。Rust外で定義された項目に対してこれを使用するのは一般的です。
関数宣言をRustで記述します。
mod ffi { /* ... */ extern "C" { pub fn acre_sem(pk_csem: *const T_CSEM) -> ER_ID; pub fn sig_sem(semid: ID) -> ER; pub fn wai_sem(semid: ID) -> ER; pub fn ref_sem(semid: ID, pk_rsem: *mut T_RSEM) -> ER; } }
Note
extern "C" { ... }
ブロックはC言語ABIに準拠した外部関数をこのモジュールに追加します。
以上で記述したFFI宣言を利用したコードを記述します。(
root_task
は Rustタスクを追加する で追加した関数です。)#[no_mangle] extern "C" fn root_task(_: usize) { unsafe { // SAFETY: 渡している `*const T_CSEM` の参照先は読み込み可能 let sem = check_er(ffi::acre_sem(&ffi::T_CSEM { sematr: 0, isemcnt: 1, maxsem: 2, })); // SAFETY: `sem` の所有権を持っているので、他のコードの動作を妨げることはない check_er(ffi::sig_sem(sem)); // SAFETY: `sem` の所有権を持っているので、他のコードの動作を妨げることはない check_er(ffi::wai_sem(sem)); let info = { let mut info = std::mem::MaybeUninit::uninit(); // SAFETY: 渡している `*mut T_RSEM` の参照先は書き込み可能 check_er(ffi::ref_sem(sem, info.as_mut_ptr())); // SAFETY: `ref_sem` が成功したので、 `info` は初期化されている info.assume_init() }; println!("Semaphore state = {info:?}"); assert_eq!(info.semcnt, 1); } } #[inline] #[track_caller] fn check_er(x: ffi::int_t) -> ffi::int_t { assert!(x >= 0, "kernel service call failed: {}", x); x }
注意
この例ではセマフォを動的生成するので、
kernel_cfg.c
(カーネル起動パラメータ) を編集してTNUM_AID_SEM
の値を1
以上に設定する必要があります。Note
FFIで宣言した関数の呼び出しは
unsafe { ... }
ブロック内でしか行えません。これはRustの言語機能による安全性の保証は外部関数には及ばないためです。unsafe { ... }
ブロックを使う際は// SAFETY:
コメントで安全性の根拠を簡潔に説明するのが慣習です。MaybeUninit
は「未初期化かもしれないメモリ」を表します。通常はRustでは未初期化の変数のアクセスはできません。しかし「含まれるT_RSEM
が未初期化である」状態はMaybeUninit<T_RSEM>
の初期化済み状態として有効です。MaybeUninit::uninit
によりこの状態のMaybeUninit
を生成します。ref_sem()
の呼出しによって有効なT_RSEM
が代入された後に、MaybeUninit::assume_init
によってこのT_RSEM
を取り出します。assert!
は指定された条件が満たされていない場合にパニックします。パニックメッセージをprintln!
と同じ書式で指定できます。#[track_caller]
[Rustリファレンス] はパニック時などに呼出し元の行番号を伝搬する属性です。println!
の書式で使用している{:?}
はT_RSEM
のDebug
トレイト実装を使用して文字列表現を生成することを表します。
実行すると、
ref_sem()
から返されたT_RSEM
の内容がログ出力先で見えるはずです。