新FFIのデザイン

FFIを刷新するために、まず、IDL(Interface Definition Language)を準備することにした。大抵のSchemeスクリプト言語FFIを備えているが、専用の考察を持ったDSLをちゃんと持っているものは無い気がする。

UCID (Unified C Interface Description)

というわけで、まずは専用のIDLをデザインしている。

(c-library (ucid test)

(type handle ;; => handle* sizeof-handle も自動生成する
      (struct
        (value void*)
        (value_length int (length/byte value))
        (flags handle_flags)
        (type handle_type)))
(type handle_type uint64 (enum
                           TYPE_A
                           TYPE_B))
(type handle_flags int (flags
                         HANDLE_ATTR_RO
                         HANDLE_ATTR_RW
                         HANDLE_ATTR_ALL))

;; 定数のimportはimplicitに行われる。書いても無害。
;; (constant/signed
;;  TYPE_A TYPE_B
;;  HANDLE_ATTR_RO HANDLE_ATTR_RW HANDLE_ATTR_ALL)

(func Open handle* ((name char* const)))
(func Close void ((handle handle*))))

型と関数を宣言できるだけのシンプルなものだが、通常のFFIにはおおよそ不要なものも記述できるようにしている。これは、IDLを単純な呼び出しだけでなく他の用途にも活用することを考慮しているため。

  • コールトレースDLLの自動生成。このIDLを処理することで、呼び出しをダンプするためのDLL/.soを自動で生成できる程度の記述力を持たせている。他にも呼び出し規約の検査などの用途も想定している。
    • DWARFから生成できるかもしれないが、そのためにはプログラムを正確に記述する必要があるので、規約を宣言する言語を別に用意するのも有用に思える。
    • APIのバリデーション、例えば、関数が仕様にないエラーを返した場合に警告する等。
  • Implicit parameters。関数の引数や構造体のフィールドの一部を補完できるように、length等の属性を付与して個々のフィールドを連携できるようにしている。
  • EnumやFlagsのデコード。通常のDWARFの範囲ではフラグのデコードに十分な情報を保持していない。フラグをEnumで宣言することで、GDBには認識させられるが、通常のAPIはフラグをEnumで持たない。

IDLをコンパイルすると、C言語ソースコードが出力される。ソースコードには構造体のアクセサや定数のエクスポートが含まれる。(Swig等と異なり、IDLコンパイラC言語をパースしない。元のヘッダはあくまでCコンパイラによって処理されるため、アノテーション等をターゲットAPIに加える必要は無い。ただし、生成されたコードを実行しなくても構造体のオフセット等が取り出せるように工夫している。)

Implicit parametersとシンボルの型変換

リッチなIDLを持つことで、C言語のプログラムをよりも快適にC言語関数が呼びだせるようになる。

単純なポインタ型でなく、専用の`box`ポインタとして引数を宣言することで、多値を返すC言語関数をより直感的に実行できる。

;; void Add(int a, int b, int* c); // *c = a + b;
(func Add0 void ((a int) (b int) (c (* int))) ;; 通常のポインタ
(func Add void ((a int) (b int) (c (box int))) ;; Box

このようにして宣言したAddは、=>を使用して、

(define add0 (pffi^ (Add0 a b c) 'ok)
(define add (pffi^ (Add a b (=> c)) c)
(let1 box (make-int-box)
  (add0 a b box)
  (int-box-ref box)) ;; cを取り出す
;; addは直接呼べる
(add a b) ;; => c

これは記述量が減る以外に、一部のboxをスタック割り当てするチャンスを作る。

  • Implicit parameters

IDLは、引数同士の関連を記述することができるので、引数の一部を省略してPFFIランタイムに任せることができる。

;; void Output(const char* str, size_t count);
(func Output void ((str char* const) (count size_t (length/byte str))))

パラメタ`count`は`str`のバイト単位での長さを格納すると約束する。呼出は<>(SRFI-26 cutに見られるような"穴"シンボル)を用いて、

(define Output (pffi^ (Output str <>))

(cutとは意味合いがあまりにも違うので、偽のようなシンボル以外か、underscoreを使用するほうが良いかもしれない。)

  • シンボルを使用した呼び出し

C APIはフラグを取るものが多い。このため、シンボルのリストをpffi手続きに渡すと、ランタイムがシンボルリストからフラグ値を演算して渡す。

;; void Check(int flags);
;; #define CHECK_A 1
;; #define CHECK_B 2
;; #define CHECK_ALL (CHECK_A|CHECK_B)
(func Check void ((flags int (flags CHECK_A CHECK_B CHECK_ALL))))

このとき、CHECK_AとCHECK_Bを渡すには、

(define check (pffi^ (Check flg))
(check '(CHECK_A CHECK_B))
  • クエリ

APIはまだ決めていないが、関数の取るフラグのリストを実行時にクエリできると便利。つまり先のCheckの例では、

(pffi-query-func-flag-list (~ Check flags)) ;; => CHECK_A CHECK_B CHECK_ALL