読者です 読者をやめる 読者になる 読者になる

C++とLuabindでインスタンスのやり取り

C++ ゲーム開発

現在の個人プロジェクトでLua組み込んでみよかーと思ったので備忘と誰かのために。

Lua Luabind on OSX

Luabind公式のソースでOSXではソースからコンパイルできなかった。Makefileをちょろっと見たけど拡張子の指定とかがOSX用で無かったり、ネットで見つかるOSXの指定オプションが無視されたりとよくわからない状態だったのでbrewからインストールする。

~$ brew install luabind

依存のboostも合わせてインストールされる。

ソースとコンパイル

メインプログラムのソースとヘッダはこんな感じ

#include <iostream>
#include <luabind/luabind.hpp>
// Luaライブラリ
extern "C" {
    #include "lualib.h"
}

これは他所サイトでもよく見かけるので解説はそれらを参考にしてください。で、コンパイル。

私の場合は動的ライブラリの位置とファイル名が
/usr/local/lib/libluabind.dylib
/usr/local/lib/liblua.5.1.5.dylib

だったので、

clang++ .main.cpp -g -O0 -std=c++11 -stdlib=libc++ -o ./a.out -llua.5.1.5 -lluabind

とライブラリ名を指定してやる。lib○○.dylibの○○部分にハイフンを付ける。dylibじゃなくファイル名がsoでも多分おなじ。
brewからインストールした場合、/usr/local/libに上記のようにlua5.1とluabindの動的ライブラリが無い場合があるかもしれない。その場合は/usr/local/Cellar以下や/usr/local/opt以下にないか確認して/usr/local/lib以下にリンクを貼ると良い。多分貼らなくても自動で見つけてくれる気がする。

C++とLuaの連携について

スクリプト言語による効率的ゲーム開発 新訂版 (LuaとC/C++連携プログラミング)

スクリプト言語による効率的ゲーム開発 新訂版 (LuaとC/C++連携プログラミング)

入門用に上記の書籍に目を通した。LuabindではなくLuaAPIを生で触る所から解説されてある。わかりやすさも心がけて書かれているのでとても読みやすい。ネットに日本語の情報が少ないので最初は書籍に頼ったほうがいいかもしれない。(私は図書館で借りてきたこの1冊だけ)

で私はイロイロだるかったのでluabindというちょっと高級なライブラリから連携を取ることにする。c++側のクラス登録も楽勝。この辺は他サイトに譲る。

luaでc++側のクラスを扱うにはluabindで登録していくだけで基本OKなんだけども1つ注意点として、Luaの知らない戻り値型をluaで受け取らせるとエラーで落ちてしまう。なので自作の巨大なクラスをLuaで自由自在に扱わせようとすると全ての型をluabindで登録しなくちゃいけない。

上記書籍では話を簡単にするために、登場するサンプルコードが極力lua側で物事を解決しようとしているので、あまり上記のような問題は検討されていない。でも一般的には、C++とLuaでインスタンスが行ったり来たりするのが現場の普通だと思われるので、いっちょテストコードを書いた。
main.cpp:

#include <iostream>
#include <luabind/luabind.hpp>
// Luaライブラリ
extern "C" {
    #include "lualib.h"
}

// Luaでインスタンスを取り扱うためのテストクラス
class TestClass {
public:
    // コンストラクタとデストラクタ
    TestClass() {}
    ~TestClass() {}
    // 文字列を返すメンバ関数
    std::string hello() {
        return "hello world";
    }
    // 数値を保持するメンバ変数
    int my_count = 0;
};

int main() {
    // Luaハンドラ(VM)とファイルオープン
    lua_State* L = lua_open();
    if (luaL_dofile(L, "./luafile.lua")) {
        // エラーは強制終了
        std::cerr << "ERROR : " << lua_tostring(L, -1) << std::endl;
        lua_close(L);
        abort();
    }
    // Luaライブラリ利用
    luaL_openlibs(L);
    // Luabindとヒモ付け
    luabind::open(L); 
    luabind::module(L) [
        // クラスの登録
        luabind::class_<TestClass>("TestClass")
            // コンストラクタ登録
            .def(luabind::constructor<>())
            // helloメンバ関数登録
            .def( "hello",&TestClass::hello)
            // countメンバ変数登録
            .def_readwrite("my_count", &TestClass::my_count)
    ];
            // Basic sf types
    // TestClassインスタンス生成
    TestClass test_instance;

    // Lua側のlua_main関数に引数でTestClassインスタンスを渡しつつ呼び出し
    luabind::call_function<void>(L, "lua_main", &test_instance); // 参照渡しにしないとコピーされるので注意
    // C++側でも値の保持を確認してみる
    std::cout << "welcome back C++" << std::endl;
    std::cout << test_instance.my_count << std::endl;

    // Lua側でTestClassインスタンスを作成してみる(戻り値はTestClassの参照)
    TestClass &test_made_lua = luabind::call_function<TestClass&>(L, "make_instance");
    std::cout << "welcome back C++ again" << std::endl;
    std::cout << test_made_lua.my_count << std::endl;

    // 用が済んだらLuaクローズ
    lua_close(L);
    return 0;
}

luafile.lua:

function lua_main(test_instance)
	print("hello from lua");
	-- インスタンスtest_instanceからTestClassメンバを操作してみる
	-- メンバ関数はインスタンス名:メンバ の構文でアクセス
	print(test_instance:hello());
	-- メンバ変数はインスタンス名.メンバ の構文でアクセス
	print(test_instance.my_count);
	test_instance.my_count = test_instance.my_count + 1;
	print(test_instance.my_count);
	test_instance.my_count = test_instance.my_count + 1;
	print(test_instance.my_count);
	test_instance.my_count = test_instance.my_count + 1;
	print(test_instance.my_count);
end

function make_instance()
	-- TestClassインスタンスをLua側で生成
	test = TestClass();
	-- 値を保持させて返してみる
	test.my_count = 999;
	return test;
end

Result:

hello from lua
hello world
0
1
2
3
welcome back C++
3
welcome back C++ again
999

ポインタは普通に渡せる

上記のコードのポイントは抜粋したcall_functionを使った2行

    luabind::call_function<void>(L, "lua_main", &test_instance);

    TestClass &test_made_lua = luabind::call_function<TestClass&>(L, "make_instance");

call_functionの引数は第一にVMのハンドラ、第二がlua側の呼び出したい関数名、第三以降にlua側の関数が受け取る引数を指定する構文になっているのですが、ここにポインタが普通に渡せます。参照で渡さないとコピーされるので注意。取り扱えるのはlualib.hで定義されている基本的な型と上記コードのluabind::class_のように教えてあげた型だけかと思われる。

2行目は戻り値として同じくTestClass型のポインタを受け取っている。

1行目があれば、c++で生成したインスタンスをlua側でゴニョゴニョさせれる。lua側のグローバル変数だかクロージャに突っ込んでおけば、C++側のentityとluaのVMを対で連携させていろいろ処理ができる。

2行目は、luaで作ったインスタンスをC++で受け取っているので、処理の要なんかをC++で受け持ちたい場合もこれで対応可能。

test_instance:hello();
test_instance.my_count;

Lua側では:コロンと.演算子でインスタンスとメンバをつなぐ。Luaで作ったインスタンスと同じ構文でC++から持ち込んだインスタンスが扱えるようです。

Luabindのヘッダはとても重いのでプリコンパイル化必須

#include <luabind/luabind.hpp>

この一文がめちゃ重い。中は魔宮のようになっていてコンパイルするとテンプレートがばんばん展開されるらしい。Luabindとトレードオフだとしても許容できないくらい重い。これ1つでコンパイルが10秒以上は伸びる。

cpplover.blogspot.jp

ここを参考にpchにすることで、1/10くらいの速度になりました。一安心。