stubIRの設計

Cソースprinter( http://d.hatena.ne.jp/mjt/20141019/p1 )の次は、stubジェネレータの入力となるS式構文(stubIR: stub intermediate representation)を設計する。
(もっとも、Cソースprinterは使わないことにした。出力がみづらくてデバッグに支障があるため。)
今回は0.2.8のスケジュール的な都合で簡略化する:

  • 特殊shim( http://d.hatena.ne.jp/mjt/20141001/p1 )はハードコードする。今回はerrnoとGetLastErrorのみ。
  • ポインタ型は自動生成しない。
  • 型のimport/exportはSchemeスタブにのみ反映し、検査しない。
  • 全てのstubIRを読み込み、一度に処理する。ユーザがstubIRを書くユースケースを(直接は)サポートしない。
  • ビットフィールドはサポートしない。
  • unionとstructは区別しない。

stubIRとそれから生成されるC stubは以下のようなパートで構成される

  1. コンパイル環境のセットアップとdefine
  2. 型定義 (メタデータ: stubIRのみ)
  3. 型レイアウト定義
  4. ncccブリッジ関数
  5. 定数export
  6. 初期化関数 (自動生成: C stubのみ)

例えば、C stubの途中に何かを#includeしたり、Cマクロを定義したりすることはできない。

ルート要素(初期化関数)

ルート要素はstubir0になる。一つのstubir0につき、一つの.cコードと.scmライブラリが生成されることになる。
0.2.8では、初期化関数に内容を記述することはできない。ライブラリ名を含むため先頭に記述するが、実際の初期化関数はCソースの末尾に生成される。

(stubir0
  csymbol ;; Cシンボル
  (<ライブラリ名シンボル> ...)
  (import <インポートするライブラリ> ...)
  (prologue ...)  ;; Cマクロの定義
  (types ...)
  (layouts ...)
  (functions ...)
  (exports ...))

prologue: コンパイル環境のセットアップとdefine

このパートは生成されるC stubの冒頭に位置し、全てCプリプロセサ指令で構成される。ここ以外のパートでCプリプロセサを使用するためには、ここで指示用のマクロを#defineしておく必要が有る。
0.2.8では、指示用のマクロは#ifdefで評価することしかできず、値を持つこともできない。指示用のマクロは適当にrenameされる可能性がある。これはamalgamation*1のサポートのため。

(prologue
  (cpp-ifdef
    "__APPLE__" ;; verbatimに挿入される
    (cpp-begin
      (cpp-define APPLE_OCL_FRAMEWORK) ;; ここではシンボルを使用する
      (cpp-system-include "OpenCL/opencl.h"))
    (cpp-system-include "CL/cl.h")))
  • シンボル
(prologue ...) ;; プロローグ部分
(cpp-define sym)
(cpp-define string) ;; POSIXのバージョン指定等直接挿入する必要のあるものはこちらを使用する
(cpp-import-predefine sym) ;; コンパイラの事前定義やincludeによる定義を期待する場合にはこちらを使用する
(cpp-if string <THEN>)
(cpp-if string <THEN> <ELSE>)
(cpp-ifdef string <THEN>)
(cpp-ifdef string <THEN> <ELSE>)
(cpp-system-include string) ;; #include <>
(cpp-include string) ;; #include ""
(cpp-begin ...) ;; 複数のCPP要素をifに展開する場合に利用する

types: 型定義

型定義は、以降のstubIRで参照される型を宣言する。
intやcharのような組込み型は特に宣言しなくても使用できる。また、型をimportすることもできる。型をimportした場合でも、プロローグには必要なincludeを毎回記述する必要がある。
基本となる型には以下の4種類が有る:

  • blob
    • integer
    • real
  • pointer

realは常にネイティブエンディアンのIEEE754表記と見做される。integerは同様にネイティブエンディアンの整数となる。blobはsizeofによってサイズを導出できることが期待される型で、Scheme側から見るとbytevectorとなる。
型定義自体はCソースを出力しない。通常のケースでは、定義された型は型レイアウト定義も持ち、sizeof等の情報をそこで提供する。
いわゆる"-of"サフィックスを持つ派生型を定義できる。

  • array-of
  • pointer-of

array-ofはScheme側にはvectorとなる。array-of型はbytelengthまたはcount制約を受け入れる。また、zero-terminate制約を付けることで、C文字列のようなゼロ終端を表現できる。
pointer-ofは型レイアウトの抽出時に使用される。従来存在した" * 付きの型の自動生成"は行わない。全ての型は命名される必要がある。
いわゆるエラーコード等意味のあるenum groupを表現するために、enum-group制約とflag-group制約が存在する。C言語は一般にネームスペースされないためenumを不適切な文脈でインスタンシエートすることは許可されている。
また、enum groupはenum以外にも使用する。これはAPIトレースをシンボル化するときや、実際のコードで利用される。

(types
  (integer char)
  (integer int)
  (integer long)
  (integer short)
  (ifdef HAS_LONGLONG
         (integer longlong "long long"))
  (real float)
  (real double))
  • シンボル
(types ...) ;; 型定義部分
(<型種別> sym <メタデータ> ...) ;; Cシンボルと一致する型、またはinternal subtypeの定義
(<型種別> sym string <メタデータ> ...) ;; Cシンボルと一致しない型の定義

;; 型種別
blob
integer
unsigned-integer
real
pointer
enum-group ;; 実際のデータはintegerとして扱われる
flag-group ;; ditto

;; メタデータ
c-struct ;; インスタンシエートに struct が必要
c-enum ;; インスタンシエートに enum が必要
c-union ;; インスタンシエートに union が必要
internal ;; クライアントコードでの生成を許可しない
zero-terminated

(array-of sym)
(pointer-of sym)
(basetype sym) ;; enum-group/flag-groupの基底型、省略時はint
(members sym ...) ;; enum-group/flag-groupのメンバ

layouts: 型レイアウトと制約定義

型レイアウト定義は、Schemeから操作することのできる構造体を定義する。構造体とunionは区別されない。単にフィールドがオーバーラップしているものとみなされる。ここでレイアウトが定義されなかった構造体はopaque(開示可能なメンバがなく、sizeofのみを持つ)として扱われる。
個々のメンバは制約(constraints)を持つことができ、制約を利用することでSchemeから当該オブジェクトを生成するときにメンバの記述を省略することができる。また、typesにおける型種別は全て制約として使用できる。

  • シンボル
(layouts ...) ;; レイアウト定義部分
(aggregate sym (<レイアウトエントリ> ...))

;; レイアウトエントリ
(型名 sym <制約> ...)
(aggregate sym (<レイアウトエントリ> ...))
(aggregate (<レイアウトエントリ> ...)) ;; 無名

;; 制約
(bytelength sym) ;; バイト長をfill
(count sym) ;; 要素数をfill
(constant INTEGER) ;; 定数をfill

functions: ブリッジ関数

nccc形式( http://d.hatena.ne.jp/mjt/20141001/p1 )の関数と通常のC関数またはマクロとのブリッジ。
引数はlayoutsにおけるメンバと同様に扱われ、制約も同様に利用できる。
0.2.8では、生成すべきブリッジは全て手動で指定する必要がある。考えられるブリッジを全部出力するので十分という説も有るが。。例えば、関数マクロをインスタンシエートするケースではtype0 forward stubやbackward stubは生成することができない。
生成されるstubは3種類考えられる。

  • type0 forward stub(f0) : 直接呼び出しを行うncccシグネチャのstub
  • type1 forward stub(f1) : 関数ポインタを暗黙の先頭引数に取るncccシグネチャのstub
  • type2 backward stub(b2) : コンテキストとnccc関数ポインタを引数に取るstub

引数のメタデータとして inout を指定すると、当該引数は出力として扱われる。0.2.8では、引数指定なし(= out)はサポートしない。また、type2 backward stubはコンテキスト引数の位置を context で指定する必要がある。
また、特殊なshimコードの生成を行わせるメタデータが2種類ある

  • errno : 暗黙の戻り値として実行後のerrnoの値またはゼロを追加
  • GetLastError : 暗黙の戻り値として実行後のGetLastErrorの値またはゼロを追加

著しくカッコ悪いが、引数の宣言に関してはC構文をシンボルで直接書くことにした。引数のattributeを良く処理する方法が思いつかなかった。型名としてリストを渡すと、verbatimに出力Cソースに挿入される。この場合の型はpointer / integer / unsigned-integerのいづれかから適当に選択される(*を含むシンボルを出力した場合はpointer、それ以外の場合はunsignedの有無で判別しunsigned-integerまたはinteger)。

(functions
  ((char*) func0 ((int a) (int b)) forward-0 forward-1)))
  • シンボル
(戻り型 sym (<引数> ...) <メタデータ> ...))

;; 引数のメタデータ
inout ;; ポインタを受けとる引数。戻り値に追加する。backwardスタブでは使用不可。
context

;; 関数のメタデータ
forward-0
forward-1
backward-2
errno
GetLastError

exports: 定数export

個々のメンバはaggregateにおけるメンバと同様に処理されるが、制約はここでは使用できない。
0.2.8では、グローバル変数のexportは読み取り専用となる。グローバル変数に書き込む必要が有るならば、それをwrapした関数を用意する必要がある。また、aggregateのメンバをexportすることはできない。

(types
  (blob FILE)
  (pointer FILE-pointer (pointer-of FILE))
(exports
  (FILE-pointer stdin)
  (FILE-pointer stdout)
  (FILE-pointer stderr))
  • シンボル
(exports ...)

(型名 sym) ;; エクスポート

*1:複数のCソースコードを連結して1つのコードにしてからコンパイルする。リンク時最適化が効くためパフォーマンス的なメリットが有る。SQLite等で使用されるテクニック。