yunibase構成変更 / yuniFFIでChickenとGambitとPicrinをサポート

Windows10のAnniversary Updateで超クッソ激烈に面倒な状況になってしまい本来の作業が全然進まないので、その隙にyunibaseをアップデートした。いろいろとbacklogが有ったので纏めて。

yunibaseのディレクトリ構成を変更

yunibaseはyuniをビルドするように変更した。これはPicrinのyuniサポートコードをPicrinと同時にビルドする必要があるため。もともとそういう処理系が出現することは想定していたけど実際にはGambitのようにコンパイル済の.soを直接loadできたり、Gaucheやchibi-schemeのように組込みのC Extensionサポートが有ったりしたため必要性が生じてこなかった。

Docker hubもちょっとドキュメントした。要するにyunibaseイメージにはビルド済の各種Scheme処理系(週1くらいでアップデート)とyuniが収録されており、dockerが使える環境であればこれをpullしてくることで直ぐにyuniライブラリを各種処理系で試すことができる。
... という環境を作っておかないとクロス処理系なライブラリってのはなかなか開発しづらいと思う。まぁyuni自体はまだ空なので今後内容を充実させていきたい。唯一実用的そうなのは本来R6RSのChezやVicareでR7RS smallライブラリが使えることか。まだパッケージャを用意していない & R6RSなんでREPLが無いのであんまり意味無いけど。
現状はyuni自体のCIくらいにしか用途が無いけど、例えばWebサーバを中で上げて各種処理系用のWeb REPLが有ると良いかな。。

生成元のソースコードが解らなくなる問題は、Dockerのコンテナにメタデータを振ることで解決した。docker commitコマンドでイメージを作っているので、LABELは -c オプションで付与する必要があった。

yunibase自体はmacOSとかFreeBSDのような環境にも対応しているけど如何せんイメージの良い配布手段が無いし、イメージを配布したところでそれを受け入れる良いCIサービスが有るわけでもないのでどうするかは考え中。たぶんローカルに構築するのかな。。

ChickenとGambitでのFFI

Rapid-GambitでGambitを使ってR7RSを実行できるようになったのでyuniFFIも移植してみた。...実はGambitとChickenは結構諸々の仕組みが似ているのでついでにChickenにも対応した。

これで、C言語で実装された処理系のうちLarcenyを除く全部でyuniFFIが使えるようになった。 ...まだABIが決まってないので呼べるライブラリは無いけど。POSIXくらいはさっさとバインディングを用意したいところ。。
Chickenはforeign-lambda、Gambitはc-lambdaを使って、C言語コードを混ぜ書きする。GaucheSagittariusのようなCソースジェネレータのある処理系と違って相当量のコードを文字列として書かないといけないのでちょっと辛い。いわゆるhere documentが有るとは言え、構文強調の類いは効かないんで。。
Gambitは処理系自体でbignumをサポートしているがChickenはそうではない(numbersモジュールの手続きを使用する必要がある - Cインターフェースから受けとる数値はnumbersの手続きで解釈できない)のでポインタを数値として直接Cコードとやりとりすることができない。Chickenの場合は、c-pointerとして専用の型が存在するのでそれを利用できる。意外なことに、bytevectorから直接c-pointerオブジェクトを生成する手段が存在しないため、yuniFFIとして自前で実装している。

(define %%yuniffi-ptr64-ref/bv
  (foreign-lambda*
    c-pointer
    ((nonnull-u8vector in)
     (size_t off))
    "void* in0;
     in0 = in + off;
     C_return((void*)(uintptr_t)(*(uint64_t*)in0));"))

ChickenもScheme側のコードではbignumが使えるので特に苦労なく実装できた。Chicken、Gambitいづれもyuniのビルド時にそれぞれの処理系を使用しなければならなくなるが、これはGaucheやchibi-schemeのようなインタプリタでも(stubライブラリの生成に使うので)同様なのでこれといって追加の考察は必要なかった。

Picrin対応

Picrin( https://github.com/picrin-scheme/picrin )はminimalisticなR7RS処理系で、yuniでサポートしている他のR6RS/R7RSに比べて:

  • (scheme char)のような必須でないライブラリの一部が欠けている - yuniでもこれらは使わないようにしたかったので特に問題にはならなかった
  • 有理数複素数がない - 同上
  • #x0aのような16進リテラルが読めない
  • bignumが無い
  • ハッシュテーブルが無い - dictionaryというシンボルをキーにしたテーブルだけが有る

といった特徴がある。Picrinのような組込み志向の処理系のサポートはyuniにとって営業上重要なので比較的真面目に対応することにした。(流石にsyntax-rulesでシンボルが使えないのは起票したけど https://github.com/picrin-scheme/picrin/issues/345 )
yunibaseへの組込みは比較的スムースにできた。ただ差分ビルドをサポートしていない(#includeしているファイルが更新されてもリビルドしない)ので毎回cleanしてからビルドしている。FreeBSDCygwinのビルドのために3点ほど修正した。

Picrinの拡張モジュールはSchemeとCの区別は無く、picrinのソースツリーにあるcontrib以下にディレクトリを掘り、ファイル nitro.mk を配置するとMakefileにincludeされるので一緒にリンクされるようにすれば良い。これは既存のcontribのコードの真似をすれば良いので直ぐ対応できた。
問題はCインターフェースで、ドキュメント通りにやっても上手くいかなかった。pic_deflibrary() → pic_defun()とするのが正しいようだ。
Picrinのユーザ定義型はCポインタと型情報のペアという非常にシンプルな構成で、処理系として専用のコンストラクタを用意する必要がなく簡単に実装できる。GCマーク用のcallbackと解放のための関数を与えてpic_data_value()で返すだけとなっている。

static const pic_data_type yunipointer_type = {
    "yunipointer",
    yunipointer_dtor,
    yunipointer_mark
};

static pic_value
ptr_value(pic_state* pic, uintptr_t val){
    // #<yunipointer val> オブジェクトを返す。ポインタ値はuintptr_tで表現できると仮定。
    return pic_data_value(pic, (void*)val, &yunipointer_type);
}

...継承のような機能は無いが、多くの場合はこれでも十分な気はする。
難しいのはbignumの無い処理系でC言語数値を扱うという課題で、yuniの想定するC言語数値は64bitあるのでPicrinの数値(fixnumやflonum)で全体を表現することはできない。というわけで、以下のような割り切りを行うことにした:

  • ポインタを含む値は常にbytevectorまたは専用のyunipointer型で扱う。途中で整数に変換するパスを残さない。(例えばnull pointerの判定等は専用APIを用意することになる)
  • 構造体内のoffsetやsizeofは数値で扱う。fixnumで扱えないようなoffsetが出てくることは多分無いので。
  • C言語マクロ定数はとりあえず数値で行ってみる。-1のような負値もとりあえずサポートする

負値のサポートは他のbignum有の処理系よりもずっと丁寧に書く必要があった。従来は、一旦unsignedで読み、最上位ビットが立っていたら負数にする:

(define (bytevector-s32-ref-le bv o)
  (let ((b (bytevector-u32-ref-le bv o)))
   (if (> b #x7fffffff)
     (bitwise-ior s32bits b)
b)))

という処理にしていた(コードはChicken用のもの)。
bignumの無いPicrinの場合、一旦unsignedで読むと -1 のような絶対値の小さな値でもオーバーフローを起こしてしまうため、

(define (bytevector-s32-ref-le bv o)
  ;; Check MSB first
  (let ((b (bytevector-u8-ref bv (+ o 3))))
   (if (> b 127)
     (exact (- 0 (bytevector-u32-ref-le/bf bv o) 1)) ;; /bfはビット反転後にunsignedとして読む手続き
     (exact (bytevector-u32-ref-le bv o)))))

最初にMSB(最上位バイト)をチェックして負数かどうかを調べ、負数の場合は全体をビット反転して読んだ後に負値に変換している。
ちなみにこの処理自体は初導入ではなく、chibi-schemeのバグに対応するために一時期workaroundとして検討していた:

yuniは何気に処理系のバグ撃墜率が高い気がする。yuni自体よりもっと規模の大きなコードはいくつかのプロジェクトで使っているけど、Scheme処理系のバグを踏んだ記憶はあまり無い。