FIELD NOTES: 書を持って街へ出よう

合同会社フィールドワークス プログラマ兼代表のブログ

C言語でOCamlデータを生成する方法のまとめ

ユーザーズマニュアルの“Interfacing C with Objective Caml”に必要なことは全部書いてあるのですが,理解するのにちょっと苦労したので,自分なりの理解をまとめておきます。
プログラミングする際に最低限必要な概念をまとめただけなので,詳細は端折っています。
正確な知識を得るためにはやはりユーザーズマニュアルも参照してください。

写真素材 PIXTA
(c) みっちー写真素材 PIXTA

基礎知識

すべてのOCamlデータは,Cのvalue型として表されます。
value型データはunboxかboxに分けられ,boxはさらにblockかobjectに分けられます。

  • unboxデータ
  • boxデータ
    • block: OCamlが管理するヒープ内に確保されたデータ
    • object: ヒープ外で確保されたデータ

value型は,32ビットまたは64ビットの整数として定義されています。
unboxデータの場合には31ビットまたは63ビットの符号付き整数が格納され,boxデータデータの場合にはポインタ(アドレス)が格納されます。
では,どうやってboxとunboxを区別しているかというと,最下位ビットが1か0かで判定しているようです。
以下は,OCamlの整数値を生成するマクロの定義です。16ビット以上のアーキテクチャでは奇数アドレスがあり得ないことを利用しているようですね。

#define Val_long(x)     (((intnat)(x) << 1) + 1)

ブロック型データは,以下のデータ構造で管理されています。

+--------+------+-------------+-------------+     +---------------+
| サイズ  | タグ  | フィールド0  | フィールド1  | ... | フィールドn-1  |
+--------+------+-------------+-------------+     +---------------+

サイズとタグを合わせてヘッダと呼びます。
サイズはフィールドがいくつあるかを示していて,0の場合もあります。
タグには,ヴァリアントの種類を区別するために,0, 1, 2, … のような整数値が割り振られます。
各フィールドには,value型データが格納されます。

基本的なデータの生成

整数

Val_int(x) マクロで,intをunboxなvalue型の値に変換します。

浮動小数点数

caml_copy_double(x) 関数により,ヒープ上に確保したblock上にdouble値をコピーします。

文字

文字コードが整数値として格納されます。
Val_int(x) マクロを使用します。

文字列

caml_copy_string(s) 関数により,ヒープ上に確保したblock上にC文字列をコピーします。

論理値

falseは整数値0,trueは整数値1で表現されます。
Val_false, Val_false マクロが利用できます。

ユニット

ユニット()は,整数値0で表現されます。
Val_int(0)が利用できます。

リスト

サイズ2のブロックをLispのconsセルに見立てた形でリストを構成します。
フィールド0がcar部,フィールド1がcdr部に相当します。

フィールド0には要素が持つ値を,フィールド1には次の要素へのポインタを格納します。
最後の要素のフィールド1の値は0になります。

配列

基本的にリストと同じ構造です。
浮動小数点の配列だけは,処理効率上の理由から特別な構造を持つようですが,詳細は調べていません。

タプル/レコード

タプルとレコードは,同じデータ構造になります。
タプルまたはレコードの要素数と同じサイズのブロックを生成します。
そして各フィールドには,定義順に要素の値を格納していきます。

ヴァリアント

ヴァリアントの場合分けに対応して,0, 1, 2, … の順にタグに整数値を割り当てます。
例えば,

type switch = On | Off;;

と宣言すると,Onにはタグ0サイズ0のブロックを,Offにはタグ1サイズ0のブロックを割り当てます。
型表現を持つヴァリアントの場合はレコードと同じ構造になりますが,実際にどの型のデータが格納されているかはタグで区別できます。

実例

Pythonの辞書/リストオブジェクトをOCamlの JSONライブラリ json-wheelで定義されているデータ型に変換するプログラムの一部を実例として示します。

Field Reports には拡張モジュールのソースが添付されています。全体のソースが必要な場合は試用版をダウンロードして入手してください(ダウンロード)。

json-wheelでは,JSONデータを以下のヴァリアントとして定義しています。

type json_type =
    Object of (string * json_type) list
  | Array of json_type list
  | String of string
  | Int of int
  | Float of float
  | Bool of bool
  | Null
準備

タグをCのenumとして宣言します。

typedef enum {
    Object,     /* Tag 0 */
    Array,      /* Tag 1 */
    String,     /* Tag 2 */
    Int,        /* Tag 3 */
    Float,      /* Tag 4 */
    Bool        /* Tag 5 */
} json_type;
整数

Pythonの整数をJSONのIntに変換しています。
サイズ1,タグ3のblockを生成して,フィールド0に整数値をセットします。

static value encode_long(PyObject* self, PyObject* obj)
{
    CAMLparam0();
    CAMLlocal1(result);

    result = caml_alloc(1, Int);
    Store_field(result, 0, Val_int(PyLong_AsLong(obj)));

    CAMLreturn(result);
}
リスト

PythonのリストをJSONのArrayに変換しています。
サイズ2のblockを生成して,フィールド0にリスト要素の値を,フィールド1に次の要素へのポインタをセットします。
リスト要素数分処理を繰り返して,数珠つなぎの構造を作成します。
最後にサイズ1タグ1のblockを生成して,フィールド0に先頭のリスト要素をセットします。

static value encode_list(PyObject* self, PyObject* obj)
{
    CAMLparam0();
    CAMLlocal3(result, cons, next);

    Py_ssize_t i;
    cons = Val_emptylist;
    next = Val_emptylist;
    result = caml_alloc(1, Array);
    for (i = PyList_Size(obj) - 1; i >= 0; --i) {
        cons = caml_alloc(2, 0);
        Store_field(cons, 0, encode(self, PyList_GetItem(obj, i)));
        Store_field(cons, 1, next);
        next = cons;
    }
    Store_field(result, 0, cons);

    CAMLreturn(result);
}
辞書

Pythonの辞書をJSONのObjectに変換しています。
キーと値の組は2要素のタプルで表現され,さらにリストの要素として格納されます。

static value encode_key(PyObject* self, PyObject* obj)
{
    CAMLparam0();
    CAMLlocal1(result);

    if (PyUnicode_Check(obj)) {
        PyObject* unicode = PyUnicode_AsUTF8String(obj);
        result = caml_copy_string(PyBytes_AsString(unicode));
        Py_DECREF(unicode);
    } else if (PyBytes_Check(obj)) {
        result = caml_copy_string(PyBytes_AsString(obj));
    } else {
     struct module_state *st = GETSTATE(self);
        PyErr_SetString(st->REPORTS_Error, "illegal arguments");
        result = Val_long(0);
    }

    CAMLreturn(result);
}

static value encode_object(PyObject* self, PyObject* obj)
{
    CAMLparam0();
    CAMLlocal4(result, cons, next, pair);

    PyObject *key, *val;
    Py_ssize_t pos = 0;
    result = caml_alloc(1, Object);
    cons = Val_emptylist;
    next = Val_emptylist;
    while (PyDict_Next(obj, &pos, &key, &val)) {
        pair = caml_alloc(2, 0);
        Store_field(pair, 0, encode_key(self, key));
        Store_field(pair, 1, encode(self, val));
        cons = caml_alloc(2, 0);
        Store_field(cons, 0, pair);
        Store_field(cons, 1, next);
        next = cons;
    }
    Store_field(result, 0, cons);

    CAMLreturn(result);
}