Saturday, February 7, 2015

Lua5.3を自力バインド

LuaとC++を自力でつないでみます。

以前はtolua++を使っていましたが、グルーコードを毎回生成するのが意外と不便でした。ヘッダの日付が変わってしまったために全体ビルドがかかってしまったりします。nmakeやバッチファイルを導入してみたり、自動生成したファイルをバージョン管理に含めるべきか悩んだりしました。結局外部定義ファイルよりはcppに直接バインド内容を書けてしまうほうが悩まないかもしれません。

例えば、WindowsのMessageBoxをLuaから使えるようにするコードです。あっけないほど簡単です。わざわざ外部のライブラリを導入する必要もないです。プロジェクトの性質によってはこれで十分用足りるでしょう。
int MesBox(lua_State *L)
{
const char* str = lua_tostring(L, 1);
MessageBoxA(nullptr, str, "LuaTest", MB_OK);
return 0;
}
void Bind()
{
lua_register(L, "MesBox", MesBox);
}
view raw mes_box.cpp hosted with ❤ by GitHub
MesBox("A message from Lua")
view raw mes_box.lua hosted with ❤ by GitHub

ここからが本題です。C++を使うからにはC++っぽいことをしたいわけです。目指す仕様は、
・C++のクラスをバインドしてLuaからインスタンスを作る
・グルーコードでnewからdeleteまで勝手にやってくれる
・ガーベージコレクションの時点でdeleteさせたい

これを実現したコードが以下です。
class MyClass
{
public:
int value;
};
static const char* myClassName = "MyClass";
static int MyClassSetValue(lua_State *L)
{
MyClass** pp = (MyClass**)luaL_checkudata(L, -2, myClassName);
(*pp)->value = (int)lua_tointeger(L, -1);
return 0;
}
static int MyClassGetValue(lua_State *L)
{
MyClass** pp = (MyClass**)luaL_checkudata(L, -1, myClassName);
lua_pushinteger(L, (*pp)->value);
return 1;
}
static int MyClassGC(lua_State *L)
{
MyClass** pp = (MyClass**)luaL_checkudata(L, -1, myClassName);
delete *pp;
return 0;
}
static int MyClassNew(lua_State *L)
{
MyClass** pp = (MyClass**)lua_newuserdata(L, sizeof(MyClass*));
*pp = new MyClass;
(*pp)->value = 0;
luaL_getmetatable(L, myClassName);
lua_setmetatable(L, -2);
return 1;
}
void BindMyClass()
{
int r = luaL_newmetatable(L, myClassName);
assert(r);
lua_pushstring(L, "__index");
lua_pushvalue(L, 1);
lua_settable(L, -3);
lua_pushstring(L, "__gc");
lua_pushcfunction(L, MyClassGC);
lua_settable(L, -3);
lua_pushstring(L, "SetValue");
lua_pushcfunction(L, MyClassSetValue);
lua_settable(L, -3);
lua_pushstring(L, "GetValue");
lua_pushcfunction(L, MyClassGetValue);
lua_settable(L, -3);
lua_pop(L, 1);
lua_register(L, "MyClass", MyClassNew);
}
view raw my_class.cpp hosted with ❤ by GitHub
local obj = MyClass()
obj:SetValue(1234)
print("My value is", obj:GetValue())
view raw my_class.lua hosted with ❤ by GitHub
My value is 1234
view raw output hosted with ❤ by GitHub

C++のBindMyClass関数でLua内にMyClassというクラスをメタテーブルで定義します。MyClassはSetValueとGetValueという2つのメソッドを持ち、__gcでガーベージコレクションの直前に呼ぶ関数があることをLuaに教えます。__indexはやや特殊ですが、MyClassのインスタンスがメソッドを探しに行くテーブルがメタテーブル自身であることを示します。これによってメタテーブル内にメソッドを定義できます。

最後のlua_registerで"MyClass"という名前のLua関数を呼ぶとC++側のMyClassNew関数がインスタンスを生成して返します。クラス名と同じ名前の関数として登録していますが関数名は何でも構いません。MyClassNewではLuaのユーザーデータを生成してC++側で生成したMyClassのポインタを格納し、先ほど作っておいたMyClassのメタテーブルを設定します。

MyClassSetValueとMyClassGetValueはLua側からC++側の変数に値を格納したり取り出したりする所です。せっかくなのでLua5.3の新機能である整数形式を試してみます。lua_Integerはデフォルトで64bitになりました。よって、int型に代入する場合キャストしないとwarningが出ます。

一見複雑ですが、アセンブリを読むような難解さはあれど覚えることはそう多くはないです。一度自力でバインドする方法を試しておくと、たとえ他のオープンソースのバインダーを使う場合でもより理解しやすくなるはずです。

GitHubはここです。

No comments:

Post a Comment