Lua

2010年06月25日

コンパイル言語 C++ とスクリプト言語 Lua を、Luabind を使って組み合わせてみる

はてなブックマーク -コンパイル言語 C++ とスクリプト言語 Lua を、Luabind を使って組み合わせてみる Check

みなさん、こんにちは。
kaoruです。

今回は、コンパイル言語 C++ とスクリプト言語 Lua を、Luabind というライブラリーで使って組み合わせて利用する方法を紹介します。

はじめに

まず、C++とLuaを組み合わせようという背景を説明したいと思います。

両者を比較するポイントとして、コンパイル言語かスクリプト言語かという点が重要な要素になります。

コンパイル言語とスクリプト言語の特徴を挙げてみました。

コンパイル言語(C++, JAVAなど)
  • コンパイル時に全ての変数の型の整合性がチェックされるので堅牢
  • 大きなデータを効率的に扱える
  • 実行速度が速い
  • 一度作ってしまえば再利用が容易
スクリプト言語(Lua、Python、PHPなど)
  • 型が動的で柔軟
  • コンパイルやインストール、システム再起動などの前準備せずに動作確認が可能
  • コーディング量が少なく、試行錯誤が簡単
  • 再利用できるように作るには工夫が必要

コンパイル言語とスクリプト言語では、得意分野が大きく違うのです。

そこで、それぞれの得意分野を生かしながら両方を組み合わせて開発をすることでよりよいものを作ることができます。

今回取り上げるLuabindはC++とLuaの間の橋渡しをするためのライブラリーです。

Luaを使ってみよう

読者の中には、Luaを使ったことがない方もいらっしゃると思いますので、雰囲気をご紹介したいと思います。

Hello worldを作ってみよう

Luaはほかのアプリケーションの中に組み込んで使うことが多いのですが、単体でも動作します。

画面にテキストを表示するだけなら1行から書けます。

print "Hello world"

簡単ですね。Luaは行末にセミコロンを書かなくてもOKです。

文とブロック

PHPなどでは、{ と } でブロックを囲みますが、Lua では do と end などで囲みます。

do
    local a = 123
    print a
end
四則演算と式

PHPなどほかの言語に比べると、!= がなくて、かわりに ~= と書かないといけないなど、微妙な違いはありますが、慣れるとあまり大きな違いはありません。

do
    local a, b = 1, 2
    if a ~= b then
        print "Mismatch"
    end
end

さらにメタテーブルという機能を使うと、オブジェクト同士の演算などをカスタマイズできるので、C++のoperatorと感覚が似ていて面白そうです。

関数の定義

関数の定義はJavascriptとよく似ています。ラムダ式も使えます。

function make_less_than(b)
    return function less_than(a)
              return a < b
           end
end

Luabindを使ってみる

簡単に使い方を紹介します。

Luabind / Lua のインクルードファイルをロードします。

#include <luabind/lua_include.hpp>
#include <luabind/luabind.hpp>

まず、Luaステートオブジェクトを作成します。

lua_State* L = lua_open();

lua.hをみると、lua_open()関数はマクロでluaL_newstate()関数に置換されるため、luaL_newstate()関数を直接呼んでもよさそうです。

ここで作成したオブジェクトは、使い終わったら、lua_close(L)関数でクローズしなければいけません。C++では、自分で閉じるのではなく、RAIIクラスやスマートポインターを使って自動的にリソース管理するほうがよいでしょう。

次に標準ライブラリーをロードします。

luaL_openlibs(L);

この関数をロードするためには追加のインクルードファイルが必要かもしれません。

extern "C" {
    #include "lualib.h"
}

Luabindを使う場合は、Luabindの初期化も忘れないように行わなければいけません。忘れるとアプリケーションがクラッシュしてしまいます。

luabind::open(L);
Luabindによる疑似クラス

Luaにはクラスの言語サポートはありませんが、メタテーブルなどの柔軟な言語機能を用いて、クラス相当のものをライブラリーで作ることができます。 今回はLuabindの機能を使ってクラスを作ってみました(yumewaza.cpp, sample.lua)。luabind::open(L)を呼び出すと、class, propertyやsuperなどのキーワードが登録され利用できるようになります。

yumewaza.cpp
#include <string>
#include <sstream>
#include <iostream>
#include <luabind/lua_include.hpp>
#include <luabind/luabind.hpp>


extern "C" {
    #include "lualib.h"
}

int main(int argc, const char *argv[])
{

    if (argc != 2) {
        std::cerr << argv[0] << " <script file.lua>" << std::endl;

        return 1;
    }

    lua_State* L = lua_open();

    luaL_openlibs(L);

    luabind::open(L);

    if (luaL_dofile(L, argv[1])) {

        std::cerr << "ERROR : " << lua_tostring(L, -1) << std::endl;

        lua_close(L);
        return 1;
    }

    lua_close(L);

    return 0;
}

sample.lua
-- 継承元がない独立したクラス MyClass
class 'MyClass'
    -- コンストラクター
    function MyClass:__init()
        self.var_a = 1
    end

    -- 関数1
    function MyClass:func01()
        print("MyClass:func01()")
    end

    -- 関数2
    function MyClass:func02()
        print("MyClass:func02()")
        self.var_a = self.var_a + 1
    end

-- MyClass を継承したクラス MyClass2
class 'MyClass2' (MyClass)
    -- コンストラクター
    function MyClass2:__init()
        MyClass.__init(self)
    end

    -- オーラーライドされた関数2
    function MyClass2:func02()
        MyClass.func02(self)
        print("MyClass2:func02()")
        self.var_a = -self.var_a
    end

do
    print "---------- test01 ---------"
    local obj01 = MyClass()
    print(string.format("self.var_a = %d", obj01.var_a))
    obj01:func01()
    print(string.format("self.var_a = %d", obj01.var_a))
    obj01:func02()
    print(string.format("self.var_a = %d", obj01.var_a))

    print "---------- test02 ---------"
    local obj02 = MyClass2()
    print(string.format("self.var_a = %d", obj02.var_a))
    obj02:func01()
    print(string.format("self.var_a = %d", obj02.var_a))
    obj02:func02()
    print(string.format("self.var_a = %d", obj02.var_a))
end
sample.luaの実行例
---------- test01 ---------
self.var_a = 1
MyClass:func01()
self.var_a = 1
MyClass:func02()
self.var_a = 2
---------- test02 ---------
self.var_a = 1
MyClass:func01()
self.var_a = 1
MyClass:func02()
MyClass2:func02()
self.var_a = -2

サンプルに示すように、classキーワードを使うことで、本当のクラスのように定義が書けるほか、継承や、メソッドのオーバーライドも可能になります。 自分で独自にクラス機構を実装することもできますが、Luabindを使っている場合は、ぜひ利用してみてはどうでしょうか。

C++のクラスや構造体をLuaで使う

C++のクラスや構造体をLua側で使えるように登録するには次のようにします。

例として次の構造体がC++側であるとします。

struct my_struct_t
{
    int var_a;
    std::string var_b;

    boost::optional<std::string> var_c;

    void method_a();

    int method_b();
    std::string method_c();
};

これをLua側で使えるようにLuabindを使って登録してみます。

luabind::module(L)
[
    luabind::class_<my_struct_t>("my_struct_t")
        .def_readwrite("var_a", &my_struct_t::var_a)
        .def_readwrite("var_b", &my_struct_t::var_b)
        .def_readwrite("var_c", &my_struct_t::var_c)
        .def("method_a", &my_struct_t::method_a)
        .def("method_b", &my_struct_t::method_b)
        .def("method_c", &my_struct_t::method_c)
];

C++のフリー関数をLuaで使う

あるいは、C++のフリー関数をLua側で使えるように登録するには次のようにします。

C++側で、次のようなフリー関数があったとすると

my_struct_t create_my_object()
{
    return my_struct_t();
}

Lua側で使えるようにするには、次のようにします。

luabind::module(L)
[
    luabind::def("create_my_object", &create_my_object)
];

C++からLuaスクリプトを実行

このような準備をした後、luaL_dostring関数を使えば、Luaのスクリプトを文字列で渡して実行できます。

    luaL_dostring(lua_state.get(),
        "function set_a(obj, value) \n"
        "    obj.var_a = value      \n"
        "end                        \n"
    );

C++からLuaスクリプトファイルをロードして実行する

あるいは、Luaスクリプトをファイルからロードするには次のようにします。

if (luaL_dofile(L, "script.lua")) {
    std::cerr << "ERROR : " << lua_tostring(L, -1) << std::endl;

    BOOST_ASSERT( false );
}

Luaの関数をC++から呼び出す

また、Luaスクリプトの中で定義されている関数をC++側から直接呼び出すこともできます。

my_struct_t obj;
luabind::call_function<void>(
    L,
    "set_a",

    boost::ref(obj),
    123
);

呼び出されるLua側のソースコードは例えば次のようになります。

function set_a(obj, value)
    obj.var_a = value
end

Lua側でC++側の変数の値を変更するには

ここで、objをそのまま渡さずに、boost::ref(obj)としているのは、objをそのまま渡してしまうと、objのコピーが作成されて、それがLua側に渡されるために、Lua側でオブジェクトのプロパティーを書き換えても、呼び出し元のobj変数は変更されないからです。

boost::refを利用すると、参照としてLua側にオブジェクトを引き渡せるので、Lua側での変更が、きちんと、C++側に反映されます。

Luaの文字列/nilをboost::optionalを使って扱う

また、数値や文字列だけではなく、nilをC++とLuaでやりとりしたい場合もあると思います。

今回は、boost::optionalを使ってC++側でnilを表現することにしました。それには、LuaとC++の双方で型をコンバートする方法をLuabindに教える必要があります。

#include <string>
#include <boost/optional/optional.hpp>
#include <luabind/lua_include.hpp>
#include <luabind/luabind.hpp>

namespace luabind

{
    template <>
    struct default_converter<boost::optional<std::string>>

      : native_converter_base<boost::optional<std::string>>
    {
        static int compute_score(lua_State* L, int index)
        {

            using namespace std;

            switch (lua_type(L, index))
            {

                case LUA_TNIL:
                case LUA_TSTRING:
                    return 0;

                default:
                    return -1;
            }
        }

        boost::optional<std::string> from(lua_State* L, int index)
        {

            using namespace std;

            switch (lua_type(L, index))
            {

                case LUA_TNIL:
                    return boost::optional<std::string>();

                case LUA_TSTRING:
                default:
                    return string(lua_tostring(L, index));
            }
        }

        void to(lua_State* L, boost::optional<std::string> const& x)
        {

            using namespace std;

            if (x)
                lua_pushstring(L, x->c_str());

            else
                lua_pushnil(L);
        }
    };

    template <>
    struct default_converter<boost::optional<std::string> const&>

      : default_converter<boost::optional<std::string>>
    {};
}

今回は、boost::optionalとLuaの文字列/nilを相互に変換していますが、C++の他の型をLuaとやりとりする方法を定義する参考になればと思います。

Luaスクリプト中で発生したエラー情報の取得

デフォルトではLuaスクリプトにエラーがある場合、あまり情報を取得することができませんが、luabind::set_pcall_callback というメソッドでエラーハンドラーを指定することができます。

今回、サンプルを作ってみました。

int lua_error_handler(lua_State* L)
{
    lua_Debug d = {};
    std::stringstream msg;

    // スタックからエラーメッセージを取得する
    std::string err = lua_tostring(L, -1);

    msg << "ERROR: " << err << "\n\nBacktrace:\n";

    for (int stack_depth = 1; lua_getstack(L, stack_depth, &d); ++stack_depth) {

        lua_getinfo(L, "Sln", &d);

        msg << "#" << stack_depth << " ";

        if (d.name)
            msg << "<" << d.namewhat << "> \"" << d.name << "\"";

        else
            msg << "--";

        msg << " (called";

        if (d.currentline > 0)
            msg << " at line " << d.currentline;

        msg << " in ";
        if (d.linedefined > 0)

            msg << "function block between line " << d.linedefined << ".." << d.lastlinedefined << " of ";

        msg << d.short_src;
        msg << ")\n";
    }

    // スタックに積まれているエラーメッセージを、新しい文字列に置換する。
    lua_pop(L, 1);
    lua_pushstring(L, msg.str().c_str());

    std::cout << msg.str() << std::endl;

    return 1;
}

luabind::set_pcall_callback(lua_error_handler);

実行例を、以下に示します。

ERROR: [string "function _set_a(obj, value) ..."]:2: attempt to index field 'var_a' (a number value)

Backtrace:
#1 <global> "_set_a" (called at line 2 in function block between line 1..3 of [string "function _set_a(obj, value) ..."])
#2 -- (called at line 5 in function block between line 4..6 of [string "function _set_a(obj, value) ..."])

1行目にエラーの内容(この例では、var_aは数値であるのに、インデックスフィールドが参照された旨が示されている。)が表示され、 Backtrace:の次の行から、呼び出し履歴が表示されています。#1によると、エラーの個所はグローバル関数の_set_aの中の2行目(それはLuaスクリプトとして渡された文字列の1?3行目で定義されている。)から呼び出されていることが分かり、#2によると、それは、5行目から呼び出されていることがわかります。

このようにエラーハンドラーを設定することで、エラーが発生した場所や、スクリプトのファイル名、関数の呼び出し履歴などを取得し、表示することができます。

Luabind がVS2010でコンパイルエラーになる場合には・・・

余談ですが、筆者の環境ではVisual Studio 2010でLuabindがコンパイルエラーになったため、Luabindのソースコードを若干変更しました。

具体的には、std::pairのコンストラクターを呼び出している個所で、C++0xに関連して、0の扱いがあいまいになってしまいコンパイルエラーになっていたので、明示的なキャストをして問題を解決しました。

diff --git a/luabind/lua_include.hpp b/luabind/lua_include.hpp
index 899df14..368b61a 100755

--- a/luabind/lua_include.hpp
+++ b/luabind/lua_include.hpp
@@ -25,8 +25,8 @@

 extern "C"
 {
-       #include "lua.h"
-       #include "lauxlib.h"

+       #include <lua.h>
+       #include <lauxlib.h>
 }

 #endif
diff --git a/src/inheritance.cpp b/src/inheritance.cpp
index 45daeb8..4c7293b 100644

--- a/src/inheritance.cpp
+++ b/src/inheritance.cpp
@@ -154,7 +154,7 @@ std::pair<void*, int> cast_graph::impl::cast(

     if (cached.first != cache::unknown)
     {
         if (cached.first == cache::invalid)
-            return std::pair<void*, int>(0, -1);
+            return std::pair<void*, int>(static_cast<void *>(0), -1);

         return std::make_pair((char*)p + cached.first, cached.second);
     }

@@ -192,7 +192,7 @@ std::pair<void*, int> cast_graph::impl::cast(

     m_cache.put(src, target, dynamic_id, object_offset, cache::invalid, -1);

-    return std::pair<void*, int>(0, -1);
+    return std::pair<void*, int>(static_cast<void *>(0), -1);
 }

 void cast_graph::impl::insert(

おわりに

駆け足で、紹介してきましたが、C++とLua、Luabindを利用した開発のイメージはつかめましたでしょうか。コンパイル言語での開発の中にスクリプト言語を取り入れると、自由度が上がり、開発の幅が広がりますので、ご興味のある方は一度試してみられるといいと思います。

LuaのAPIをC言語から呼び出す際には、Luaスタックを介してやりとりをしなければいけないので、 少々面倒くさい面があるのですが、C++からLuabindを利用すると、Luaスタックの操作はほとんど ライブラリー側で面倒をみてくれるので、本当にやりたいことだけを書けばよくなっています。

まだまだ、書ききれなかったことはたくさんありますが、LuaやLuabindのオフィシャルサイトの他、最近は、ユーザーのブログや書籍もたくさんありますので、困った時にはいろいろと探してみるといいかもしれません。

最後に、オフィシャルサイトのリンクを掲載しておきます。

http://www.lua.org/
Luaのサイト 最新のLuaがダウンロードできます

http://www.rasterbar.com/products/luabind.html
Luabindのサイト 最新のLuabindがダウンロードできるほか、ドキュメントがとても参考になります。

はてなブックマーク -コンパイル言語 C++ とスクリプト言語 Lua を、Luabind を使って組み合わせてみる Check
日時: 2010年06月25日 16:22 | コメント (0) | トラックバック (0)
ゆめみ深田浩嗣のブログ Mercury mobmail
YUMEMI Labs Sweet