言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

Pepsi オブジェクトの構造

イアンのやつ(pepsi & coke 又は Ian System 略して IS) の復習です。IS には次の二つの言語が含まれます。

ここで、 http://piumarta.com/svn2/idst/trunk/ Rev 347 における Pepsi のオブジェクト構造をメモします。なお、Pepsi は型が無く、Smalltalk とは違ってインスタンスに対するクラスも無いという建前ですが、面倒なのでプロトタイプの事を型と書きます。

C から見た Pepsi オブジェクト

以下サンプルコード。

{ import: Object }

MyObject : Object (first second) "Object を継承した型の作成"

MyObject new
[
    self := super new.
    first := 42.
    second := 'Hello'.
]

MyObject say [ ^ StdOut println: second ]

Pepsi は C 言語のプリプロセッサとして実装されています。型定義は次のように変換されます。

struct t_MyObject {
  struct _vtable *_vtable[0];
  oop v_first;
  oop v_second;
};

普通に構造体を作るだけです。トリッキーなのが _vtable メンバの長さが 0 なので、最初の要素は _vtable では無くて v_first です。_vtable(メソッド辞書) はポインタ - 1 の場所に置くという約束になっているので、obj->_vtable[-1] のようにアクセスします。メソッド say の定義は次のようになります(デバッグ用コードを除く)。関数名は(プロトタイプ名__メソッド名) という単純な命名規則でつけられています。メソッド名に _ が含まれる場合は文字コード 5f に置換されます。

static oop MyObject__say(oop v__closure, oop v_stateful_self, oop v_self)
 {
  oop _1= 0;
  oop _2= 0;
  _1= v_StdOut;
  _2= ((struct t_MyObject *)v_stateful_self)->v_second;
  _1=_sendv(s_println_, 2, _1, _2);
  return _1;
 }

この場合、v_stateful_self と v_self は同じく レシーバを指します。後は構造体にキャストして値を取り出すだけです。Pepsi は変数に型の無い言語なので、すべての型は oop 型として扱われます。次に、C 言語とのインタフェースについて書きます。Pepsi では {} の中に C プログラムを書くことが出来るので、これを利用して C から Pepsi オブジェクトにアクセスしてみます。

[
    | obj |
    obj := MyObject new.
    {
        // Pepsi 変数 obj は C から v_obj と見える。
        struct t_MyObject * obj = (struct t_MyObject *) v_obj;
        struct t_String * s = (struct t_String *) obj->v_second;

        // 整数 42 は、C から 85 (= 42 * 2 + 1) として見える。
        printf("obj.first = %i\n", (int) obj->v_first >> 1);
        printf("obj.second = %s\n", (char *) s->v__bytes);

        // 以下同じ事を三通りの方法で書きます。
        _sendv(s_say, 1, v_obj); // マクロで書いた場合。
        MyObject__say((oop) 0, v_obj, v_obj); // 直接呼び出し。
        {
            struct __send _s= { s_say, 1, v_obj }; // メソッド名, 引数の内容と数を設定
            _imp_t function = _libid_bindv(&_s); // 実際に呼ばれる関数を求める。
            function(&_s, _s.receiver, _s.receiver); // 実装 MyObject__say の呼び出し。
        }
    }.
]

出力

obj.first = 42
obj.second = Hello
'Hello'
'Hello'
'Hello'
  • Pepsi オブジェクトは整数かポインタのどちらか
    • 整数の場合は左シフトして最下位ビットを立てる
    • ポインタは再帰的に Pepsi オブジェクトを指すか、適当なメモリを参照
    • メモリを確保するのは _libid_balloc を使う(実体は GC_malloc_atomic)
  • メソッド呼び出しは _sendv マクロを使う。
    • 第二第三引数にレシーバを置いて自動生成された関数を呼び出しても良い。

Pepsi オブジェクトのリフレクション機能

Pepsi にはデバッグ機能がありませんが、将来使える便利なリフレクション機能があります。データ構造の定義は object/id/id.h にあります。

スロット

新しい型を定義すると、_sizeof _debugName _slots の三つのメソッドが勝手に生成されます。例えば次のようなものです。

static size_t MyObject___5fsizeof(oop _closure, oop v_self) { return sizeof(struct t_MyObject); }
static char *MyObject___5fdebugName(oop _closure, oop v_self) { return "MyObject"; }
static struct __slotinfo *MyObject___5fslots(oop _closure, oop v_self) { static struct __slotinfo info[]= { { "first", 0, 4 }, { "second", 4, 4 }, { 0, 0, 0 } };  return &info[0]; }

Object に _slots を使ってメソッド -> (オフセット -> サイズ) の辞書を作成する slots メソッドが用意されています。これを使ってオブジェクトのスロットとその内容を列挙するメソッドを作ってみます。

Object inspect
[
    | slots value |
    StdOut nextPutAll: self debugName println.
    StdOut nextPutAll: '----------'; cr.
    slots := self slots.
    slots keys do: [:key |
        StdOut nextPutAll: key, ' : '.
        value := self _oopAt: (slots at: key) key / 4.
        StdOut nextPutAll: value printString; cr].
]

[ MyObject new inspect ]

出力

MyObject
----------
first : 42
second : 'Hello'
メソッド

次に、メソッドの構造を調べます。オブジェクトの内部構造はfunction/objects/_object.st に定義されていますが、{ external } から { internal } に記された内容はヘッダで、実装は object/id/libid.c です。どういう技を使ってるのか知りませんが libid.c で定義した構造体定義だけ _object.st (から生成された _object.so.c) が書き換えます。メソッドが格納されている vtable に関する定義を抜粋します。

typedef oop (*_imp_t)(struct __send *_send, ...);

struct t__closure {
  struct _vtable *_vtable[0];
  oop v__method; // 関数オブジェクト。実際は _imp_t 型
  oop v_data;
};
struct t__vector {
  struct _vtable *_vtable[0];
  oop v__size; // 最大容量
};
struct t__vtable {
  struct _vtable *_vtable[0];
  oop v__tally;
  oop v_bindings; // 実際は t__vector 型
  oop v_delegate; // 委譲するときここに親の vtable を指定
};

これらの構造体を舐める事で、あるオブジェクトに定義されたメソッドを列挙する事が出来ます。

[
    | t bindings |
    t := MyObject new _vtable.
    bindings := t bindings.
    {
        int i;
        struct t__vtable * vtable = (struct t__vtable *) v_t;
        struct t__vector * bindings = (struct t__vector *) v_bindings;
        for (i = 0; i < ((int) vtable->v__tally); i++) {
            struct t__assoc * assoc;
            assoc = * (struct t__assoc * *) (bindings + 1 + i);
            struct t_Symbol * key = (struct t_Symbol *) assoc->v_key; // メソッド名
            struct t__closure * closure = (struct t__closure *) assoc->v_value;
            _imp_t method = (_imp_t) closure->v__method; // 関数ポインタ (GDB で見ると関数名が分かる)
             printf("%s: %x\n", key->v__elements, method);
        }
    }
]

出力

_sizeof: 804fe14
_debugName: 804fe1e
_slots: 804fe28
new: 804fe32
say: 804ff41

まとめ

Pepsi オブジェクトを C 言語から扱う方法、Pepsi のスロットとメソッドに動的にアクセスする方法について書きました。あとはスタックフレームの情報が分かればデバッグ環境が作れそうですが、これは難しそうです。Pepsiコンパイル言語ですが、IS の目的はこれらのオブジェクトを動的に作られた jolt プログラムで操作する事なので、今度そういう事も書きます。