橋渡しとしてのAPI

前回 の続き. たぶんこれで本件に関する一応の最終回.

APIは世界の境界線

異なる世界観の端境にあって,両者の言い分を理解して橋渡しをする. API の設計は,端的には橋渡しに尽きる.

API の直交性など,細かく言うと設計上気をつけるべき点はある. しかし,それは,より美しく使いやすい API であるための付加価値である. 大事だけれども本質ではない.

API の代表例として,OS が提供するものがある. システムコールとかサービスコールという名で呼ばれる.

フリーダムなユーザランドと,好き勝手されては困るOSとの間の橋渡しをする. 一例として,リアルタイムカーネル TOPPERS/JSP のサービスコール関数の冒頭を引用する.

心配ない. RTOSのコードなんて読んだことが無いよ,という方でもざっくりと理解できれば十分.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
 *  メールボックスへの送信
 */
#ifdef __snd_mbx

SYSCALL ER
snd_mbx(ID mbxid, T_MSG *pk_msg)
{
    MBXCB   *mbxcb;
    TCB *tcb;
    ER  ercd;
    
    LOG_SND_MBX_ENTER(mbxid, pk_msg);
    CHECK_TSKCTX_UNL();
    CHECK_MBXID(mbxid);
    mbxcb = get_mbxcb(mbxid);
    CHECK_PAR((mbxcb->mbxinib->mbxatr & TA_MPRI) == 0
        || (TMIN_MPRI <= MSGPRI(pk_msg)
            && MSGPRI(pk_msg) <= mbxcb->mbxinib->maxmpri));

    t_lock_cpu();

まず,関数に入った直後で,CHECK_* マクロの洗礼がある. ここで,アプリケーションから渡された引数に,OS内部にとって不都合がないかどうかを調べる. マクロだから見えないが,不都合な引数があった場合には,即座にエラーリターンとなる.

次に,t_lock_cpu() で,割込みが発生しない状況にする. つまり,カーネルが CPU を独り占めする. ここから先はカーネルの世界である.アプリケーションからは不可侵.

このような引数チェックは,API が備えるべき基本的な機能だ. API の向こうは,全く違った世界観で動いている. つまり,悪意,未必の故意,不理解により,有り得ないような引数で呼び出される可能性がある. API は,どんな引数で呼び出されるかは判らないし,信用してもいけない. 上記のリストはシンブルなほうで,マルチプロセッサやメモリ保護など加わればその分だけ,チェックの量は増える.

呼び出し元を信用しない. これが,単なる関数呼び出しとAPIとを分ける,ほぼ唯一の,しかし決定的な違いとなる. APIの設計経験が浅い人は,呼び出し元を信用しがちである. そして,呼び出し元のアプリケーションのせいで起きたバグなのに,APIの内側でのバグだとの冤罪を受け,無罪証明のために膨大な時間を費やしたりする.

API と”ヒゲ”とC言語

API の内外は,異なる世界観である. だから,相手側の世界には,なるべく自分側の世界観を押し付けないように設計すべきだ.

けれども,何事にも限度というものがある.

例えば,mruby の API には,ほぼ全てに mrb_state 型のポインタがついている.

1
2
3
4
5
6
/* mruby/include/mruby.h から引用 */
typedef mrb_value (*mrb_func_t)(mrb_state *mrb, mrb_value);
struct RClass *mrb_define_class(mrb_state *, const char*, struct RClass*);
struct RClass *mrb_define_module(mrb_state *, const char*);
mrb_value mrb_singleton_class(mrb_state*, mrb_value);
void mrb_include_module(mrb_state*, struct RClass*, struct RClass*);

これを,C99 標準には mrb_state なんて型はない,など言って無理やり合わせようとするのは,筋が良くない考え方だ.

1
2
/* こういう変更はよくない */
typedef mrb_value (*mrb_func_t)(void *mrb, mrb_value);

(C言語の基礎が解ればすぐに判るだろうけれども,念の為に理由を示すならば) mrb_state * から void * に型を変更することで,コンパイラによる型チェックが効かなくなり,mruby の内側を危険に晒す.

こういった,API を跨いで漏れだしてしまう変数型を,私は密かに「ヒゲ」と呼んでいる. 個人的に用いている語で,たぶん他の誰にも通じない.

「ヒゲ」は可能な限り少ないほうが,APIとしては優れた設計と言える. C言語の標準ライブラリは,コンパイラが生成する型を完全に理解していて,C言語向けのAPIとしては優れている. POSIX の各種 API は,独自の型はあるものの,それらの命名規則はC言語の標準ライブラリを踏襲しており,優れている.

しかし,あったほうが魅力的だったり安全になったりする「ヒゲ」もある. mruby の C言語APIにある,mrb_state ポインタがその例といえる. Windows API は,独自型の乱発など,現代的なC言語仕様から見ると眉をひそめたくなる部分もある. しかし,ISO C 標準よりも先に存在していたAPI仕様なので,仕方がない部分もある.安全を求めた結果の「ヒゲ」の部類だ. μITRON仕様も Windows API と同様.

そして,「鼻毛」もある. 「鼻毛」は,APIの内外にある世界観,ならびに言語仕様を考えた上で,どう考えても魅力がない…というか残念というか危険結果しか想像できない場合. 前回取り上げた,size_t の代わりに mrb_int を使おうとした例がこれに当たる…

…のだが,「ヒゲ」が「鼻毛」か否かは,立ち位置によって,変わりうる.

長さに関する立ち位置の違い

当然ながら Ruby は C言語ではないので,size_t なんていう型はない.符号なし整数型もない.Fixnum 型となる.

1
2
3
4
mirb - Embeddable Interactive Ruby Shell

> [].length.class
 => Fixnum

mruby の内部では,Fixnum の値は mrb_int 型として格納される.

1
2
3
4
5
6
7
8
9
/* include/mruby/value.h から引用 */
typedef union mrb_value {
  union {
    void *p;
    struct {
      unsigned int i_flag : MRB_FIXNUM_SHIFT;
      mrb_int i : (MRB_INT_BIT - MRB_FIXNUM_SHIFT);  /* ←これが Fixnum の実体 */
    };
    struct {

したがって,文字列の長さも,mrb_int 型で格納されている.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* include/mruby/string.h から引用 */
struct RString {
  MRB_OBJECT_HEADER;
  union {
    struct {
      mrb_int len; /* ←これが長さ */
      union {
        mrb_int capa;
        struct mrb_shared_string *shared;
      } aux;
      char *ptr;
    } heap;
    char ary[RSTRING_EMBED_LEN_MAX + 1];
  } as;
};

ここで,視点の差に基づく見解の相違について,可能性が見えてくる.

C言語標準の側から見れば size_t を使わないのはどう考えでも「鼻毛」なのだが,敢えて,Ruby の内側から見てみよう. 外側にあるC言語の連中は,mrb_int を使いやがれ.お前らmrubyに依存しているんだろう? という意見には,一定の合理性がある.

ああ,Matz氏とは面識もあるし,ジェントルな方である. 上記太字は,私が相当に盛った表現だ. 皆さんご存知だろうが一応補足.

表現はさておき,世間一般の論として「APIの内側と外側で,どちらが主人でどちらが従属か」というのは,API設計で,必ず出てくる悩みどころといえる.

API の主従と設計

実例を挙げる.

μITRON仕様OSは,UW, UH, UB など,uint32_t, uint16_t, uint8_t で済む型を,独自に定義している. これは,C99標準よりも遥かに前からμITRON仕様が存在したためである. 同様のオレオレ型定義は Windows などにも見られる.

しかし,μITRON仕様の系譜を汲む TOPPERS カーネルは,第2世代カーネル (ASPカーネルベース)を境に, μITRON仕様流の定義を捨てた. 現世代のTOPPERSカーネルは,全てC99標準に準拠したデータ型で API を定義している. (構造体などで,独自定義の型もある.)

一方,同じくμITRON仕様の系譜を汲む T-Kernel は,今でもμITRON仕様の流儀で API のデータ型を定義している.

T-Kernel は,OSが主であるという考え方であるといえる. この場合,しもべたるアプリケーションは,OSが定義した型に則るのがスジである. 他環境で動くライブラリを移植する際に悩ましかろうが,そんなことは知ったことではない.

一方,TOPPERSの第2世代以降の仕様は,API仕様から見ると,アプリケーション,もしくはC言語処理系の都合が主である. 仕様策定者たちの正確な意図は判らない. size_tなどコンパイラが生成する型との摺り合わせについて考えたり,MISRA系のコーディングガイドラインとの整合性を考えたり,いくつかの要因があるのだろう. しかし,結果として,ベアメタル(フリースタンディング)環境のC言語アプリケーションのための, スレッドライブラリとして,TOPPERSカーネルのAPIは,自然にフィットする. 他のライブラリを応用する際にも,ライブラリがC99が推奨するデータ型にそっている限り,悩む箇所は多くない.

T-kernel と TOPPERS という,2つのカーネルのAPIは,その視点の違いで,使う型が異なる. 同じRTOS仕様を起源とし,今でも提供する機能に大差がないにも関わらず,である. この例は,APIの主従関係の捉えかたがデータ型の選び方に影響することを,示している.

mruby と C言語アプリの主従,そしてAPI設計

というわけで,mruby へのコミットで,size_t から mrb_int へ変えたいと思ったことには,一定の妥当性がある, というか,API設計をするものとして,気持ちは判る. Matz 氏は,自他共に認める「Ruby のパパ」であり,彼が Ruby 世界を中心に mruby を考えるのは仕方がない. 仕方がないというか,そういう立ち位置でいて頂かないと皆困る.

一方で,mruby は,アプリケーションに組込むライブラリであり,そのAPIは,結合先の言語(今回の場合はC言語)を無視することができない. C言語もれっきとした言語であり仕様標準があり,コーディングパターンもある.

片方の世界でベストプラクティスだったとしても,片方の世界でアンチパターンになることは,少なくない. API設計の最も難しく,かつ醍醐味であるところ,それは,2つの世界を理解して橋渡しすることにある.

ここまでザザッと説明して,最後に2つほど質問.

mruby で mrb_int を使うのが「鼻毛」なのに,μITRON仕様やWindows API では「鼻毛」にはならないのは何故か? これをスラっと回答できるのは,少なくともデータ型に関しては API 設計をするに十分なスキルのある人であろう.

μITRON仕様のAPIには,「鼻毛」と呼ぶのにふさわしいサービスコール群がある.それはどれか. 本稿読者の多くはμITRON仕様を知らないかもしれないが,批判な目で知らないAPIレビューできるのは,API設計者に 必要なスキルである.

てな感じで,結論があるような,無いような,けむに巻いた感じで,一連の話は終わり.

今回はC言語APIを題材にしたが,ビルド構成ファイルである build_config.rb に与える機能の話など,Ruby を使った DSL の設計に関わる話題もある.でもそちら方面は私の得意分野ではないので,割愛.

mruby の面白いところは,Matz 氏が作った世界である Ruby は当然のこととして,プルリクを送る開発者たちの多様性にあると個人的には思っている. Cのほうが得意な人.C++のほうが得意な人.運用系への適用に興味を持つ人,gemsを量産する人,JITに魂を売った人,ちっちゃいものクラブ会員,などなど. こういう状況だと,mruby に興味がなくても,プルリクを眺めているだけで,プログラミングの学習になりうる. 専門分野に属すると,その分野の定石みたいなものに縛られてしまう. 今のところ,mruby には,それがない.

大した分量ではない 2つのコミットから,ここまで話を伸ばせる. 他のコミットも,たとえ数行の変更でも,深い洞察の末だったりすることも,しばしばある. C言語を覚えたけれども,その先の学習で伸び悩んでいるという向きにとって,mruby はなかなかよい教材だろうと思う.

最近,涼風 ( @suzukaze_jp )氏が,mruby masterの変更履歴を定期的にまとめているようなので,気が向いたらご参考に.