"引数オブジェクト"と構造体補完

PFFIを考える上で、C関数への"引数オブジェクト"とC構造体は同じオブジェクトとして取り扱うほうが単純化できると気付いた。
関数の引数も、構造体も互いに関連を持つ複数の値の集合(aggregate)であることは同じなので、同じ手続群で管理出来る方が都合が良い。

引数オブジェクトと構造体オブジェクト

構造体オブジェクトは特に説明の必要は無い気がする。構造体オブジェクトは、C構造体を直接的に表現するもので、Scheme側からはaggreagateオブジェクト(= R6RS用の互換実装ではrecord)に見える。
引数オブジェクトは、あるC関数の引数が構造体一つであったと仮定したときに定義される構造体と言える。

int makeWindow(const char* title, int title_len); /* 元の関数 */

typedef struct {
   const char* title;
   int title_len;
} makeWindow_args;
int makeWindow(makeWindow_args args); /* 引数オブジェクト化したもの */

C関数の呼び出し中のGCによって引数として使用されているオブジェクトが回収されることを防ぐために、引数オブジェクトはGC root setの一部とする。
引数オブジェクトの存在を仮定することで、implicit parameters( http://d.hatena.ne.jp/mjt/20130811/p1 )の実現は構造体補完の仕組みをそのまま流用できる。

構造体補完(書き出し)

構造体補完はかなり複雑な仕組みなので、読み出しと書き出しで分割する。たぶん別々の用語を当てるほうが良いと思う。
書き出し方向の構造体補完(interpolation)は省略した構造体要素を自動生成する(構造体が引数オブジェクトであった場合は、省略した引数を自動生成する)。
値の補完を行うタイミングは基本的に処理系が決定する。たとえば、lengthを計算する必要があって、かつ、構造体がlengthの補完を必要とする場合は、lengthが決定したタイミングで補完がおこなわれる。
... もちろんこれを完全に実装するのはかなり面倒なので、実際には非常に単純なタイミングで行われる。つまり、書き出し方向の構造体補完は、構造体をexport(= bytevectorやC stackオブジェクトに変換する)タイミングで行われる。これは無駄な演算が生じるが、多くのシチュエーションでは問題にならない。

  • 補完が行えない場合の扱い

補完が行えない場合も、現状エラーにならない。補完を明示的に行わせる構文を用意するのは正しいかもしれない。

(struct/type Point
  ((cbSize  int (size-of Point))  ;; Pointのサイズで補完
   (version int (constant 2));; 定数2で補完
   (x       int)
   (y       int)))
 
(make Point               ;; => Pointオブジェクト
      (x 120)
      (y 240)
      (_ cbSize version)) ;; cbSizeとversionを明示的に補完(今回の補完は常に成功するので省略できる)

(define p (make Point))   ;; 構造体Pointを未定義値で定義

(~ p 'x := 120)
(~ p 'y := 240)
(~ p 'cbSize := _)        ;; cbSizeをこのタイミングで補完(キーワード _ を代入)

構造体補完(読み出し)

読み出し方向の構造体補完(parse)は、構造体をimportする、つまりbytevectorからSchemeオブジェクトに変換する場合に行われる。
例えば、TLV型の構造体はlengthとして指定されたフィールドを読み出し、そのlengthを使用してbytevectorを生成する。
多くの読み出し方向の補完は、書き出し補完のためのannotationがそのまま使用できる。補完が失敗する場合(e.g. 定数として宣言されたversionフィールドが正しくないようなケース等)に無視するかどうかは微妙な問題となる。FFIとしての用途は単純に無視すれば良いことが多い。

読み出し補完と書き出し補完の統合

引数オブジェクトとして使用される構造体には読み出し補完と書き出し補完の両方が適用される。例えば、呼び出し側がバッファサイズを指定し、APIが指定された大きさを上限として値を返すようなケースでは、引数オブジェクトは入力と出力の両方が保持されることになる。
ここのチョイスは難しい。つまり、

  • A) 引数オブジェクトをmutableにする

C APIの引数は本質的にmutableであるケースもあるため、これを避ける事は難しい。この選択をライブラリデザインの中心にするかどうかは絶妙なポイントとなる - 引数オブジェクトの寿命がAPIの呼び出しを超えることになる。
今回はこちらを選択している。

  • B) 引数オブジェクトを入力オブジェクトと出力オブジェクトに分割する

これは、immutableな引数を認識する必要がある。(C APIでは、API上のconst修飾子で行うことができるが、これが常に正しいとは限らないという実用上の問題がある。)
Aに耐えるシステムを設計するのはそれなりに難易度が高い。パケット処理システムでいえば、入力パケットを加工して出力する可能性を検討するのと同義になる。多くのプロトコル実装では(非常にresource constrainedな環境でもない限り)これを行わない。