Gauche:FFIと型表現
Shiro(2026/01/15 04:55:16 UTC): 再訪。以前書いた時より型については少し整理が進んだ。
Gaucheレベルで見える型:
- Prescriptive types (Gaucheレベルでインスタンスを作り出す元になるもの)
- classes
- Descriptive types (値の取り得る範囲や適用し得る操作を制限するもの。印sタンスのCPL中には現れない)
<descriptive-type>: type constructorで作られるやつ。(</> <string> <integer>)とか。適合するインスタンスの型は<string>や<integer>などの prescriptive type。<native-type>: Cとのインタフェースに使われる、Schemeの型の部分型。<uint8>とか。適合するインスタンスの型は<integer>など。<proxy-type>: 内部的に、<descriptive-type>からPrescriptive typeを間接参照するためのもの (クラス再定義対策)
(stub typeについてはgenstubの中でしか扱われないので、Gaucheレベルでは見えない)。
さて、FFIでのやりとりにおいて、単純な型は<native-type>がそのまま使える。複合型はどう扱うべきだろうか。
- pointer
- function pointer
- struct, union
- array
これらも<native-type>のサブタイプにしてしまうというのがひとつの方法。GaucheのインスタンスとしてはForeignPointerがあって、それが単純型か、Cの関数か、集合型を指す。<native-type>の型情報はForeignPointerのattributeに持たせる。
利点は
- foreignなデータとGaucheのインスタンスが明確に分離できる
- ForeignPointerのクラスを、それが指す先の型とは直交する形にできる。例えばdlsym経由で得られるForeignPointerは指す先の型に関係なく
<dlptr>クラス、というふうに。 - pointer identity (Cレベルのポインタが同じであれば、Schemeレベルでも
eq?になる) を保ったまま型情報をすげ替えられる (キャストしたい場合とか)。
もうひとつの方法は、複合型についてはGaucheのクラスにしてしまうというもの。そうすると
Gaucheレベルでmakeができる。
利点は
- foreign functionに渡すための構造体や配列をGaucheのインスタンスのように作って準備できる。(ただし、GCからの保護を考えるとそれが必ずしも便利とは限らないかも)。
is-a?による型検査とか、メソッドディスパッチが使える。
前者の方法だと、Gauche側から触る時は常に1段間接的に色々やる感じになる。例えばforeign functionから受け取った構造体をGauche側であたかもGaucheのオブジェクトのようにスロットアクセスしたかったら、ラッパークラスを作ってvirtual slotで要素アクセサを呼び出すとか。
ただ、foreign objectはライフサイクルが違ったりするから、明確に分けてた方が混乱が少ないという 考え方もできる。
2026/01/15 07:36:26 UTC: FFIデータ型として、複合型に埋め込まれた複合型と、複合型に含まれるポインタから指される複合型を区別する必要がある。 (e.g. struct { struct { ... } embed; } outer; vs.
struct { struct { ... } *pointed; } outer;.
Gaucheのレイヤでは複合型はScmObjから間接参照されることになるので、outerからembedを取り出した
場合もGaucheのインスタンスは「embedへのポインタ」を保持していることになる。
ならば、pointedを取り出した場合は「native pointerオブジェクトへのポインタ」を保持する
インスタンスになるべきか。
なんてことを考えると、全部ForeignPointerで保持してnative型情報で区別する(embedは
<native-struct>、pointedは<native-pointer struct>を型として持ち、
値はどちらもinner structのアドレスを保持する) 方が扱いやすそうだな。
Shiro(2021/05/03 08:43:27 UTC): Gauche内でCの型表現に関係する箇所がいくつかあるんだけど、必要に迫られてアドホックにやってるのでバラバラ。FFI導入を機に整理したい。
今あるコンポーネント
Stub type
define-cprocの引数と戻り値で使う ::<int> など。これはSchemeとCのブリッジの役割も担っている。
gauche.cgen.type- 各stub typeは
<cgen-type>のインスタンス - Schemeオブジェクトの型検査、Cへのunboxing、CからのboxingのCコードを生成する
- 具体的なCのデータ表現は知らない (Cコードを生成した後はCコンパイラに丸投げ)
- Schemeの型とCの型は m-to-n。
- Schemeに対応するものしか考えてないので、Cで表現可能な型全てを表現できるわけではない。
CiSE type
CiSEでCの型を表記する表現。::long とか ::(const char*) とか。
gauche.cgen.cise- CiSEはこの型表現の中身はチェックしていない。ただテキストとして扱ってるだけ。なのでユーザ定義型とかトラックする必要もない。
- Cで表現できる型はだいたい書ける。
ftype
バイナリデータを扱うために書きかけになってるもの。
binary.ftype- データレイアウトを定義して、バイナリデータからフィールドを抽出したりセットしたりするのが目的
ftypeレコードのインスタンス- 各インスタンスはendianやalignment、構造体内でのオフセットなどの情報を保持。
- まだ基本型とstructしかサポートしてない
C parsed type representation
lang.c でCコードをパーズする際の型情報の表現。
lang.c.type- S式による表現
- Cで表現可能な型は全部カバー。そのためちょい冗長。
- typedefはトラックしてる (でないとパーズできないので)
- 実行アーキテクチャから中立。
Foreign pointer
実行時に外部オブジェクトへのポインタを扱う仕組みだが、attributeに型情報を載せることは可。
今のところ、外部ライブラリごとにカスタムでインタフェースをCで書いてるので、 汎用的に型情報をメタに扱う仕組みはない。
Native call
ネイティブ関数を呼び出す低レベルAPI
- ABIの対応する型だけトラック。
FFIで必要なもの
- 外部から与えられる情報
- 外部間数ごとのインタフェース宣言 : 型の簡潔な表記。CiSE表記が多分ベスト。
- ヘッダファイルをパーズしたもの:
lang.cの表現
- 内部で必要なもの
- Native callの型情報へのマッピング。これは実行アーキテクチャ依存。
- aggregate objectについて、uvectorにpack/unpackするためのftype
- pointerについてはforeign pointerとして見せるが、それにくっつけるための型情報
ftypeを軸にするのが良さそう。
- CiSE表記→ftype
lang.c表現→ftype- ftype→Native call type
- foreign pointerにftypeをアタッチするユーティリティ
ここまでftypeが重要ならコアに組み込むべきか。