はじめに
Rustの勉強をしたくて、根っからの組込み屋である僕が題材を探した結果、TOPPERSを題材にすることにした。
作った結果がもし誰かの役に立つとしたらまずはASPカーネルがよい。
ということで、TOPPERS/ASP上でRustアプリが動くようにもろもろ整えることにした。
完成したら当然公開する予定だが、途中の経過を(全部ではないが)記すことにする。
ターゲット
Wio Terminalを使う、よってRustのターゲットは thumbv7em-none-eabihf
としている。
作る範囲
RustからTOPPERS/ASPを使用するために必要な、
- データ型
- APIラッパー
- システムサービスのラッパー
- sample1のRust実装
をまずは作成する。
最終的にはカーネルのビルドも統合したいがもう少しあとの話。
Rustの機能を使うことでより便利になるみたいな付加価値はもっとあとの話。
実装ポリシー
- 使う人が
unsafe
を意識しなくていいようにする
TOPPERS/ASPはC言語で作られているので、各種APIは unsafe
ブロックで
呼び出しを書かなればならない
使う方はわざわざ面倒なので、そこは隠蔽する(必要な人は勝手に自分でも呼び出し口作れるし)
- ポインタは極力隠蔽する
- ref_xxx系を除いては戻り値を値(ERとポインタで渡す引数の組)で返す
- Rustできれいに書けるような箇所は記述を省力化する ※途中の学習コストはいとわない
syslogのラッパーを書く
画面で結果を確認するには兎にも角にもsyslogが必要になるので最初に作成した。
オリジナルのsyslog
2系統ある。
- 固定長引数版: _syslog_X(X = 0..6)
- 可変長引数版
よく使うのは可変長引数版かなと思うのでこちらだけまずは実装する。
処理は「SYSLOG構造体にフォーマット文字列と値を詰め、sys_write_logを呼ぶ」。
Inline void
_syslog_1(uint_t prio, uint_t type, intptr_t arg1)
{
SYSLOG logbuf;
logbuf.logtype = type;
logbuf.loginfo[0] = arg1;
(void) syslog_wri_log(prio, &logbuf);
}
void
syslog(uint_t prio, const char *format, ...)
{
SYSLOG logbuf;
va_list ap;
uint_t i;
char c;
bool_t lflag;
(略)
}
Rustで実現する場合の制約
Rustで可変長引数を取る関数は作れない。
ただしマクロでなら書ける。ROMを多少食うことになるが昨今大きな問題ではないだろう。
実装
syslog本体
#[macro_export]
macro_rules! toppers_syssvc_syslog{
($prio : expr, $fmt : expr, $($arg : expr),*) => {
let ini_ary = {
let mut ary : [u32; 6] = [0; 6];
ary[0] = concat!($fmt, '\0').as_bytes().as_ptr() as u32;
let mut _index = 1;
$(
{
ary[_index] = $arg;
_index = _index + 1;
}
)*
ary
} ;
let mut _syslog = toppers::syssvc::Syslog {
logtype : toppers::syssvc::LOG_TYPE_COMMENT,
logtim : 0,
loginfo : ini_ary
};
unsafe{
let _ = toppers::syssvc::syslog_wri_log($prio, &_syslog);
}
};
}
名前にはパス名が使えないのでパス記法の::を_に置き換えている(この辺拡張されないかな)。
他のライブラリとの衝突のリスクを考えると、少し長いほうが良い。
宣言だが、可変長引数なのでこんな書き方をする。
($prio : expr, $fmt : expr, $($arg : expr),*) => {
^^^^^^^^^^^^^^^^^
}
強調部分が可変長引数の書き方。最初は魔法のように見えた。
この部分を展開する記述は以下の部分。
$(
{
ary[_index] = $arg;
_index = _index + 1;
}
)*
配列 ini_ary
を一度0初期化し、その上からマクロ引数で上書きし、配列を戻す。この書き方はここにあるテクニックを使った。
Rustの配列初期化は頑固なので素晴らしくチェックが行き届いて、配列は初期値をきちんと埋めなければならない。使わない位置は0にしつつ、可変長引数で受け取った部分(表示する値部分になる)を格納する。
配列0番目はフォーマット文字列が入る。
Rustの文字列には0終端がなくUTF-8ということで、'\0'の付加とbyte配列への変換を行っている。
次にSyslog構造体を先の配列も使って初期化する、Syslog構造体の定義は以下の通り。
pub struct Syslog {
pub logtype: u32,
pub logtim: u32,
pub loginfo: [u32; TMAX_LONINFO],
}
本来はターゲットによってlogtimの方とloginfoは異なってくるはずだが、現状は32bit処理系を対象にしている。
最後に、unsafeブロックでくるんでsyslog_wri_logを呼び出す。
sample1側の実装
上記のsyslogを使って、sample1ではログメッセージを表示したり、APIのエラーを表示したりする。
APIのエラー表示がどこでどんなエラーが起きたか表示するようになっているSVC_PERRORである。
正しく行数を出すために一度マクロを間に挟む。
Rustで行やファイルの文字列を得るマクロは file!()
line!()
となる。また、式を文字列に展開するためのマクロがstringify!()マクロである。
※ここは1ライナーで書かないと、行数がずれるので注意が必要
macro_rules! svc_perror {
($expr : expr) => {
svc_error_output(
LOG_ERROR,
concat!(file!(), '\0'),
line!(),
concat!(stringify!($expr), '\0'),
$expr,
);
};
ここで生成した文字列を使って、評価結果がエラー(<0)ならばsyslogを実行する関数を書く。
pub fn svc_error_output(prio: u32, file: &'static str, line: u32, expr: &'static str, ercd: Er) {
if ercd < 0 {
toppers_syssvc_syslog!(
prio,
"%s (%d) reported by `%s' in line %d of `%s'.",
ercd_to_strptr(ercd) as u32,
sercd(ercd) as u32,
expr.as_bytes().as_ptr() as u32,
line,
file.as_bytes().as_ptr() as u32
);
}
}
型変換をして先のマクロを呼ぶ。 ercd_to_strptr
はエラーコードから文字列の生成をしている。
Er型にtraitを実装して ercd.to_string()
の方がおしゃれなので、直す予定。
sercd
はビットシフトをするだけの関数なので、割愛する。
さて、 ercd_to_strptr
はオリジナルでは下記のような記述である。C言語の限界というかこれ以上書きようがない(マクロの魔法はあるかもしれないが)。
const char *
itron_strerror(ER ercd)
{
switch (MERCD(ercd)) {
case E_OK:
return("E_OK");
case E_SYS:
return("E_SYS");
case E_NOSPT:
:
}
Rustで書く場合、syslogの実装で書いたように、UTF-8 -> ASCIIをしたり、終端文字をつけたりする処理が長ったらしくなるので、下記のマクロを用意した。可変長引数でエラーコードをまとめて受け取り、パターンマッチを生成する。
#[macro_export]
macro_rules! ercd_to_strptr{
($x : expr, $str : expr, $($ercds : path),+) => {
match mercd($x)
{
$(
$ercds => $str = concat!(stringify!($ercds),'\0').as_bytes().as_ptr(),
)+
_ => $str = "unknown error\0".as_bytes().as_ptr(),
}
};
}
これを使って、ごそっと生成する。
pub fn ercd_to_strptr(ercd: Er) -> *const u8 {
let strptr: *const u8;
ercd_to_strptr!(
ercd, strptr, E_OK, E_SYS, E_NOSPT, E_RSFN, E_RSATR, E_PAR, E_ID, E_CTX, E_MACV, E_OACV,
E_ILUSE, E_NOMEM, E_NOID, E_NORES, E_OBJ, E_NOEXS, E_QOVR, E_RLWAI, E_TMOUT, E_DLT, E_CLS,
E_WBLK, E_BOVR
);
strptr
}
生成コードはオリジナルと同じ構造になる(cargo expandで展開した結果)。
pub fn ercd_to_strptr(ercd: Er) -> *const u8 {
let strptr: *const u8;
match mercd(ercd) {
E_OK => strptr = "E_OK\u{0}".as_bytes().as_ptr(),
E_SYS => strptr = "E_SYS\u{0}".as_bytes().as_ptr(),
E_NOSPT => strptr = "E_NOSPT\u{0}".as_bytes().as_ptr(),
E_RSFN => strptr = "E_RSFN\u{0}".as_bytes().as_ptr(),
E_RSATR => strptr = "E_RSATR\u{0}".as_bytes().as_pt
:
}
長くなったが、APIを失敗したときの表示ができた。
E_ID (-1) reported by `Asp::act_tsk(100)' in line 253 of `src/main.rs'.
まとめ
表示系ができたので、本格的な移植の目処が立った。
Rustを学びながらなのでだいぶ遠回りはしているが、いい勉強になっている感じ。
特に可変長引数マクロ、マクロのパターンマッチはハマった分理解が深まった。
Rustを触った感想としては、 ビルドが通ればちゃんと動く
。
ビルドが通るまではコンパイラがかなり強敵だが、C言語のようにあっさり吹き飛ぶ感じではない。
もちろん実機での苦労が少ないほうがいいので、こっちが好みである。
次はASPを使うためのC言語バインディング周りを書こうと思う。