Lua 技術ノート 5

C++をLuaにバインドするためのテンプレートクラス

by Lenny Palozzi

概要

このノートでは、C++クラスをLuaにバインドする方法について説明します。Luaはこれを直接サポートしていませんが、低レベルのC APIと拡張メカニズムを提供しており、可能になっています。私が説明する方法は、LuaのC API、C++テンプレート、およびLuaの拡張メカニズムを利用して、小さくシンプルながらも効果的な静的テンプレートクラスを構築し、クラス登録サービスを提供します。この方法は、クラスにいくつかの制限を課します。つまり、シグネチャ`int(T::*)(lua_State*)`を持つクラスメンバ関数のみを登録できます。しかし、後ほど説明するように、この制限は克服できます。最終的な結果は、クラスを登録するためのクリーンなインターフェースと、Luaにおけるクラスの使い慣れたLuaテーブルセマンティクスです。ここで説明するソリューションは、私が書いたLunaと呼ばれるテンプレートクラスに基づいています。

問題点

LuaのAPIは、C++クラスをLuaに登録するように設計されていません。シグネチャ`int()(lua_State*)`を持つC関数のみを登録できます。つまり、`lua_State`ポインタを引数として受け取り、整数を返す関数です。実際、Luaが登録をサポートする唯一のCデータ型はそれだけです。他の型を登録するには、Luaが提供する拡張メカニズム(タグメソッド、クロージャなど)を使用する必要があります。C++クラスをLuaに登録できるようにするソリューションを構築するには、これらの拡張メカニズムを使用する必要があります。

解決策

この解決策を構成する4つのコンポーネントは、クラス登録、オブジェクトインスタンス化、メンバ関数呼び出し、およびガベージコレクションです。

クラス登録は、クラス名でテーブルコンストラクタ関数を登録することによって行われます。テーブルコンストラクタ関数は、テーブルオブジェクトを返すテンプレートクラスの静的メソッドです。

注:静的クラスメンバ関数は、シグネチャが同じであればC関数と互換性があるため、Luaに登録できます。以下のコードスニペットはテンプレートクラスのメンバ関数であり、「T」はバインドされるクラスです。

  static void Register(lua_State* L) {
    lua_pushcfunction(L, &Luna<T>::constructor);
    lua_setglobal(L, T::className);

    if (otag == 0) {
      otag = lua_newtag(L);
      lua_pushcfunction(L, &Luna<T>::gc_obj);
      lua_settagmethod(L, otag, "gc"); /* tm to release objects */
    }
  }
オブジェクトインスタンス化は、ユーザーがテーブルコンストラクタ関数に渡した引数をC++オブジェクトのコンストラクタに渡し、オブジェクトを表すテーブルを作成し、クラスのメンバ関数をそのテーブルに登録し、最後にテーブルをLuaに返すことによって行われます。オブジェクトポインタは、インデックス0のテーブルにユーザーデータとして格納されます。メンバ関数マップへのインデックスは、各関数に対してクロージャ値として格納されます。メンバ関数マップについては後で詳しく説明します。
  static int constructor(lua_State* L) {
    T* obj= new T(L); /* new T */
    /* user is expected to remove any values from stack */

    lua_newtable(L); /* new table object */
    lua_pushnumber(L, 0); /* userdata obj at index 0 */
    lua_pushusertag(L, obj, otag); /* have gc call tm */
    lua_settable(L, -3);

    /* register the member functions */
    for (int i=0; T::Register[i].name; i++) {
      lua_pushstring(L, T::Register[i].name);
      lua_pushnumber(L, i);
      lua_pushcclosure(L, &Luna<T>::thunk, 1);
      lua_settable(L, -3);
    }
    return 1; /* return the table object */
  }
C関数とは異なり、C++メンバ関数は、関数を呼び出すためにクラスのオブジェクトが必要です。メンバ関数呼び出しは、オブジェクトポインタとメンバ関数ポインタを取得し、実際の呼び出しを行う関数によって「サンク」する(間接的に呼び出す)ことによって行われます。メンバ関数ポインタは、クロージャ値によってメンバ関数マップからインデックスされ、オブジェクトポインタはインデックス0のテーブルから取得されます。Luaのすべてのクラス関数は、この関数を使用して登録されることに注意してください。
  static int thunk(lua_State* L) {
    /* stack = closure(-1), [args...], 'self' table(1) */
    int i = static_cast<int>(lua_tonumber(L,-1));
    lua_pushnumber(L, 0); /* userdata object at index 0 */
    lua_gettable(L, 1);
    T* obj = static_cast<T*>(lua_touserdata(L,-1));
    lua_pop(L, 2); /* pop closure value and obj */
    return (obj->*(T::Register[i].mfunc))(L);
  }
ガベージコレクションは、テーブルのユーザーデータにガベージコレクションタグメソッドを設定することによって行われます。ガベージコレクタが実行されると、「gc」タグメソッドが呼び出され、オブジェクトが単純に削除されます。 「gc」タグメソッドは、新しいタグを使用してクラス登録時に登録されます。上記のオブジェクトインスタンス化では、ユーザーデータにタグ値が付けられます。
  static int gc_obj(lua_State* L) {
    T* obj = static_cast<T*>(lua_touserdata(L, -1));
    delete obj;
    return 0;
  }
それを踏まえると、クラスが準拠しなければならないいくつかの要件があります。注:これらの要件は私が行った設計上の選択です。コードを少し調整するだけで、異なるインターフェースを選択できます。

`Luna::RegType`は関数マップです。`name`は、メンバ関数`mfunc`が登録される関数名です。

  struct RegType {
    const char* name;
    const int(T::*mfunc)(lua_State*);
  };
C++クラスをLuaに登録する方法の例を次に示します。`Luna::Register()`を呼び出すと、クラスが登録されます。これはテンプレートクラスの唯一のパブリックインターフェースです。Luaでクラスを使用するには、テーブルコンストラクタ関数を呼び出すことによってインスタンスを作成します。
  class Account {
    double m_balance;
   public:
    Account(lua_State* L) {
      /* constructor table at top of stack */
      lua_pushstring(L, "balance");
      lua_gettable(L, -2);
      m_balance = lua_tonumber(L, -1);
      lua_pop(L, 2); /* pop constructor table and balance */
    }

    int deposit(lua_State* L) {
      m_balance += lua_tonumber(L, -1);
      lua_pop(L, 1);
      return 0;
    }
    int withdraw(lua_State* L) {
      m_balance -= lua_tonumber(L, -1);
      lua_pop(L, 1);
      return 0;
    }
    int balance(lua_State* L) {
      lua_pushnumber(L, m_balance);
      return 1;
    }
    static const char[] className;
    static const Luna<Account>::RegType Register
  };

  const char[] Account::className = "Account";
  const Luna<Account>::RegType Account::Register[] = {
    { "deposit",  &Account::deposit },
    { "withdraw", &Account::withdraw },
    { "balance",  &Account::balance },
    { 0 }
  };

  [...]

  /* Register the class Account with state L */
  Luna<Account>::Register(L);

  -- In Lua
  -- create an Account object
  local account = Account{ balance = 100 }
  account:deposit(50)
  account:withdraw(25)
  local b = account:balance()
Accountインスタンスのテーブルは次のようになります。
  0 = userdata(6): 0x804df80
  balance = function: 0x804ec10
  withdraw = function: 0x804ebf0
  deposit = function: 0x804f9c8

解説

C++テンプレートの使用を好まない人もいるかもしれませんが、ここではうまく適合しています。最初は複雑に見えた問題に対して、迅速で緊密な解決策を提供します。テンプレートを使用することにより、クラスは非常に型安全になります。たとえば、メンバ関数マップで異なるクラスのメンバ関数を混在させることは不可能であり、コンパイラがエラーを報告します。さらに、テンプレートクラスの静的設計により、使いやすくなっています。終了時にクリーンアップするテンプレートインスタンス化オブジェクトはありません。

サンクメカニズムは、クラスの中核であり、呼び出しを「サンク」します。これは、関数呼び出しが関連付けられているテーブルからオブジェクトポインタを取得し、メンバ関数ポインタのメンバ関数マップをインデックスすることによって行われます。(Luaテーブルの関数呼び出し`table:function()`は、`table.function(table)`の構文糖です。呼び出しが行われると、Luaは最初にテーブルをスタックにプッシュし、次に引数をプッシュします)。メンバ関数のインデックスはクロージャ値であり、最後に(引数の後)スタックにプッシュされます。最初は、オブジェクトポインタをクロージャとして使用していました。つまり、インスタンス化されたクラスごとに、関数ごとにオブジェクトポインタ(void*)とメンバ関数のインデックス(int)の2つのクロージャ値を持つことになります。これはかなりコストがかかると考えられましたが、オブジェクトポインタへの迅速なアクセスが可能です。また、ガベージコレクションのためにテーブル内のユーザーデータオブジェクトが必要でした。最終的に、オブジェクトポインタのテーブルをインデックスしてリソースを節約し、その結果、関数呼び出しのオーバーヘッド(オブジェクトポインタのテーブルルックアップ)が増加しました。

すべての事実を考慮すると、この実装では、メンバ関数のインデックスを保持するためのクロージャ、ガベージコレクションのための「gc」タグメソッド、およびテーブルコンストラクタとメンバ関数呼び出しのための関数登録など、Luaで使用可能な拡張メカニズムのごく一部のみを使用しています。

なぜシグネチャ`int(T::*)(lua_State*)`を持つメンバ関数のみを登録できるようにするのでしょうか?これにより、メンバ関数はLuaと直接やり取りできます。Luaから引数を取得して値を返し、Lua API関数を呼び出すなどです。さらに、登録されたC関数と同一のインターフェースを提供するため、C++を使用したいユーザーにとって容易になります。

弱点

このテンプレートクラスソリューションは、前述の特定のシグネチャを持つメンバ関数のみをバインドします。したがって、既にクラスを作成済みの場合、またはLuaとC++の両方の環境でクラスを使用する予定がある場合は、これが最適なソリューションではない可能性があります。概要で、これが実際には問題ではないと説明すると述べました。プロキシパターンを使用すると、実際のクラスをカプセル化し、ターゲットオブジェクトへの呼び出しを委任できます。プロキシクラスのメンバ関数は、引数と戻り値をLuaとの間で強制変換し、呼び出しをターゲットオブジェクトに委任します。実際のクラスではなく、プロキシクラスをLuaに登録します。さらに、継承も使用できます。プロキシクラスが基本クラスを継承し、関数呼び出しを基本クラスに委任します。ただし、1つの注意点があります。基本クラスにはデフォルトコンストラクタが必要です。プロキシのコンストラクタ初期化リストで、Luaから基本クラスへのコンストラクタ引数は取得できません。プロキシパターンは私たちの解決策であり、これでC++とLuaの両方でクラスを使用できますが、そうすることでプロキシクラスを作成および保守する必要があります。

オブジェクトは作成時に単に`new`を使用して作成されます。オブジェクトの作成方法に関するより詳細な制御をユーザーに提供する必要があります。たとえば、ユーザーはシングルトンクラスを登録したい場合があります。1つの解決策として、オブジェクトへのポインタを返す静的`create()`メンバ関数をユーザーが実装することです。このようにして、ユーザーはシングルトンクラスを実装したり、単に`new`を使用してオブジェクトを割り当てたり、その他のアクションを実行できます。オブジェクトポインタを取得するために、`new`ではなく`create()`を呼び出すように`constructor`関数を変更できます。これにより、クラスに多くのポリシーが追加されますが、はるかに柔軟性が高くなります。ガベージコレクション用の「フック」も一部のユーザーには役立つ可能性があります。

結論

このノートでは、C++クラスをLuaにバインドする簡単な方法について説明しました。実装は非常にシンプルであるため、独自の目的に合わせて変更でき、同時に一般的な用途も満たすことができます。tolua、SWIGLua、およびこのようないくつかの小さな実装など、C++をLuaにバインドするための他の多くのツールがあります。それぞれに独自の強み、弱点、および特定の問題への適合性があります。このノートが、より微妙な問題に光を当ててくれることを願っています。

約70行のソースコードからなるテンプレートクラスの完全なソースコードは、Luaのアドオンページから入手できます。

参考文献

[1] R. Hickey, Callbacks in C++ using template functors, C++ Report February 95


最終更新日:2003年3月12日(水)11:51:13 EST lhf