2011/07/04

Lua が気になるお年頃

突然ですが、Lua が気になるお年頃がやってきました。たぶん思春期です。

TIOBE Programming Community Index for June 2011 によると、Lua は人気の高いプログラミング言語・第 10 位だとか。

派手さや嫌味のないシンプルでスマートな言語でありながら、連想配列クロージャなどのナウい仕様も備わっており、さらに数あるスクリプト言語の中でも屈指の実行速度を誇っているそうです。

まぁここまでは人から聞いた話ですが、聞けば聞くほど気になってしまい、たまたま Lua も手元にある事だし(なんであるの!?)、せっかくだからちょっとつまみ食いしてみる事にしました。

ちなみに、今回は Lua 5.1.3 に、Visual Studio 2008 Professional Edition を使いました。たぶんもっと新しいバージョンも探せばどこかに転がっていると思います。



【メタテーブルはおもしろい】

練習がてら、Lua のメタテーブルを使って「ベクトルクラス」的なもの(PVector)を作ってみました。

さっきも書きましたが、Lua は動的型付けと柔軟なテーブルが特徴的なスクリプト言語です。C 言語では様々な型を集成するために、構造体を定義する必要がありましたが、Lua ならばひとまとめにしたいデータを全てテーブルにぶち込んでおくだけでよいのでラクといえばラクです(まだ静的型付け言語に慣れた身としては、正直『何が入っているのか判らない気持ち悪さ』も多少ありますが)。

Lua におけるテーブルにはいくつかの糖衣構文が用意されていて、たとえばキー("key")を用いてテーブル(table)を参照する table["key"]table.key と等しく、さらに、キーが関数である場合、その呼び出し table.key(table)table:key() に等しいのだそうです。この仕組みを駆使すれば、OOP におけるクラスにかなり近いものが作れます。

さらに、メタテーブルというやや高度な仕組みがあり、これを用いる事で要素を参照する際の挙動をカスタマイズできたり、C++ でいうところの演算子をオーバーロードを Lua で実現したりできるようになるのです。

-- PVector クラス
PVector = {}

-- コンストラクタ
function PVector:new(x, y, z)
    -- インスタンスとして使用するテーブル
    local t = {}
    
    -- メンバ変数の初期化
    t.x = x or 0
    t.y = y or 0
    t.z = z or 0
    
    -- 
    PVector.__index = PVector
    setmetatable(t, PVector)
    
    return t
end

--[[ メンバ関数 ]]

-- 新しい値のセット
function PVector:set(newX, newY, newZ)
    self.x = newX or 0
    self.y = newY or 0
    self.z = newZ or 0
end

-- 文字列に変換
function PVector:to_string()
    return "(" .. self.x .. ", " .. self.y .. ", " .. self.z .. ")"
end

-- 「+」演算子のオーバーロード
PVector.__add = function(v1, v2)
    return PVector:new(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z)
end

コンストラクタ(PVector:new() 関数)では、setmetatable() を用いてインスタンスを表現するテーブル t に、メタテーブル PVector を関連付けています(ちなみにこのチュートリアルでは、setmetatable(t, {__index = PVector}) 的な書き方も紹介されています。こちらもおおよそ同じように振る舞います)。

これによって、返却されたテーブル t を経由して、PVector の各種メソッドを呼び出したり、PVector のメタメソッド(__add)によって、+演算子を用いたインスタンス同士の加算ができるようになります。

Lua の関数は、仮引数と実引数の数が一致していなくても特に問題はありません。たとえば、関数 PVector:new(x, y, z) に 2 つの引数を渡して呼び出した場合、最後の z 要素には nil が代入されます(今回は要素が最大 3 であるため、このような書き方をしましたが、完全に可変長の引数をとる関数の場合は、function f(...) のように、... を用いて記述します。このとき関数側では、引数群は { ... } というテーブルに格納される事になります)。

これによって関数のオーバーロードができなくなりそうな気もしますが、同じく Smalltalk の流れを汲む Objective-C だってその制約を持っていながら、NEXTSTEP や Mac を支え続けてきた実績がありますので、必ずしもこの仕様が言語としての欠陥に直結するわけではないのかな、と。

閑話休題。実引数が少ない場合、足りない分は関数呼び出し時に nil で埋められるという話ですが、nil が代入されたままだと計算時に厄介ですから、コンストラクタでは便宜的に 0 を代入する事にします。判定には if 文を用いてもよいのですが、Lua にはより簡潔な構文が存在します。

t.x = x or 0

これは、(C言語っぽく書くと) if (x != nil && x != false) { t.x = x; } else { t.x = 0; } と本質的に等価です。

で、このクラスを実際に使うとこんな感じになりました。
たしかに、(1, 2, 3) + (4, 5, 6) = (1 + 4, 2 + 5, 3 + 6) = (5, 7, 9) なので、ちゃんと+演算子が使えている事がわかります。

ちなみに継承なんかも、スーパークラスのインスタンステーブルに追加機能をぺたぺたくっつけていけばできそうな気がします



【C++ にバインド】


【C++ 側で定義した関数を Lua から呼び出す】

Lua はちっちゃな言語なので、単体で大きなプログラムを開発するにはやや不向きです。

どちらかというと、C 言語や C++ など、生産性の低い高級言語の補助に使うと幸せになれそうです。

そこで、試しに C++ の関数を Lua から呼び出す実験をしてみました。

// ========================================
// あらかじめ、lua.hpp へのインクルードパス、
// lua5.1.lib へのライブラリパスを通しておこう
// ========================================

#include <stdio.h>
#include <lua.hpp>

// ========================================
// 引数の値を表示するだけの関数
// ========================================
int TestFunction(int i) {
    printf("ここは C の関数です。引数の値は %d です。\n", i);
    return 30;  // 適当な値を返す
}

// ========================================
// TestFunction()関数を
// Luaから呼べるようにするためのグルー関数
// ========================================
int TestFunctionGlue(lua_State* L) {
    // 第1引数を取得
    int index = (int)lua_tonumber(L, 1);

    // それを元に、関数呼び出し
    int ret = TestFunction(index);

    // スタックをクリア
    lua_settop(L, 0);

    // 戻り値をスタックに載せる
    lua_pushinteger(L, ret);

    return 1;   // 戻り値の数(1個)
}

int main() {
    // Lua の初期化関連
    lua_State* L = lua_open();
    luaL_openlibs(L);

    // Lua を実行する
    luaL_dostring(L, "print('こんにちは、Luaです')");

    // ここから本番。グルー関数を登録
    // Lua側からCの関数を呼べるようになる
    lua_register(L, "TestFunction", TestFunctionGlue);

    // ----------------------------------------
    // テスト
    // ----------------------------------------
    int top = lua_gettop(L);    // スタックポインタを保存
    int ret = luaL_dostring(L,
        "val = TestFunction(100)"
        "print('ここは Lua です。戻り値は'..val..'です。')");
    if(ret != 0)
        printf("error: %s\n", lua_tostring(L, -1));
    lua_settop(L, top);         // スタックを元に戻す

    getchar();
    return 0;
}

このプログラムは、引数や戻り値の受け渡しを伴う C++ の関数を Lua から呼び出しています。実行したい関数は TestFunction() なのですが、そのままでは Lua からこの関数をコールする事ができないので、C++ と Lua の仲立ちをするグルー関数TestFunctionGlue())を作って、それを Lua にバインドしています。

C++ と Lua のデータの受け渡しには、スタックと呼ばれるデータ構造を用います。これは、データの表現形式が異なる C と Lua を仲介するために Lua 側が用意したメモリ領域で、関数の引数やら戻り値やらはここに積まれます。

というわけで、処理が正常に終了しようがするまいが、スタックの状態をちゃんと元に戻さないといずれスタックオーバーフローしてしまいますので、上記のコードでは lua_gettop() / lua_settop() で挟み打ちにして、スタックが爆発しないようにしています。

ところで、このプログラムを Visual Studio 2008 Professional Edition でビルドしようとしたら大変なことになりました(エラーメッセージがあまりにもひどいので折りたたんでおきます)。
1>------ すべてのリビルド開始: プロジェクト: LuaTest, 構成: Debug Win32 ------
1>プロジェクト 'LuaTest'、構成 'Debug|Win32' の中間出力ファイルを削除しています。
1>コンパイルしています...
1>main.cpp
1>マニフェストをリソースにコンパイルしています...
1>Microsoft (R) Windows (R) Resource Compiler Version 6.0.5724.0
1>Copyright (C) Microsoft Corporation.  All rights reserved.
1>リンクしています...
1>LIBCMT.lib(_file.obj) : error LNK2005: ___iob_func は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(crt0dat.obj) : error LNK2005: __amsg_exit は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(crt0dat.obj) : error LNK2005: __initterm_e は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(crt0dat.obj) : error LNK2005: _exit は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(crt0dat.obj) : error LNK2005: __exit は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(crt0dat.obj) : error LNK2005: __cexit は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(fflush.obj) : error LNK2005: _fflush は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(invarg.obj) : error LNK2005: __invoke_watson は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(crt0init.obj) : error LNK2005: ___xi_a は既に MSVCRTD.lib(cinitexe.obj) で定義されています。
1>LIBCMT.lib(crt0init.obj) : error LNK2005: ___xi_z は既に MSVCRTD.lib(cinitexe.obj) で定義されています。
1>LIBCMT.lib(crt0init.obj) : error LNK2005: ___xc_a は既に MSVCRTD.lib(cinitexe.obj) で定義されています。
1>LIBCMT.lib(crt0init.obj) : error LNK2005: ___xc_z は既に MSVCRTD.lib(cinitexe.obj) で定義されています。
1>LIBCMT.lib(tidtable.obj) : error LNK2005: __encode_pointer は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(tidtable.obj) : error LNK2005: __decode_pointer は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(hooks.obj) : error LNK2005: "void __cdecl terminate(void)" (?terminate@@YAXXZ) は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(winxfltr.obj) : error LNK2005: __XcptFilter は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(crt0.obj) : error LNK2005: _mainCRTStartup は既に MSVCRTD.lib(crtexe.obj) で定義されています。
1>LIBCMT.lib(errmode.obj) : error LNK2005: ___set_app_type は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(mlock.obj) : error LNK2005: __unlock は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(mlock.obj) : error LNK2005: __lock は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(setlocal.obj) : error LNK2005: __configthreadlocale は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(setlocal.obj) : error LNK2005: _setlocale は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(dosmap.obj) : error LNK2005: __errno は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(strftime.obj) : error LNK2005: _strftime は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(lconv.obj) : error LNK2005: _localeconv は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(atox.obj) : error LNK2005: _atoi は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(getenv.obj) : error LNK2005: _getenv は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(tolower.obj) : error LNK2005: _tolower は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LIBCMT.lib(strtol.obj) : error LNK2005: _strtoul は既に MSVCRTD.lib(MSVCR90D.dll) で定義されています。
1>LINK : warning LNK4098: defaultlib 'MSVCRTD' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。
1>LINK : warning LNK4098: defaultlib 'LIBCMT' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。
1>C:\Users\tercel\Documents\Visual Studio 2008\Projects\LuaTest\Debug\LuaTest.exe : fatal error LNK1169: 1 つ以上の複数回定義されているシンボルが見つかりました。
1>ビルドログは "file://c:\Users\tercel\Documents\Visual Studio 2008\Projects\LuaTest\LuaTest\Debug\BuildLog.htm" に保存されました。
1>LuaTest - エラー 30、警告 2
========== すべてリビルド: 0 正常終了、1 失敗、0 スキップ ==========
うひー∩( ・ω・)∩ なにこれありえない。

Lua のライブラリ(lua5.1.lib)を自前の処理系でリビルドしたら、きれいさっぱりエラーが消えて実行できるようになりました。めでたし。


【Lua 側で定義した関数を C++ から呼び出す】

今度は逆に、Lua 側で関数を作って、それを C++ から呼び出してみました。

Lua の方はちょっとだけ長くなったので、以下のようにスクリプトファイル script.lua にまとめて書きました。

--[[ てきとう Lua 関数 ]]

-- 引数ふたつ、戻り値ひとつ
function TestLuaFunction1(x1, x2)
    print("「もしもし。こちら Lua です。")
    print(" 受け取った引数は "..x1.." と "..x2.." です」\n")
    return 100      -- また適当な値を返す
end

で、これを呼び出す C++ 側のコードはこんな感じ。

// ========================================
// あらかじめ、lua.hpp へのインクルードパス、
// lua5.1.lib へのライブラリパスを通しておこう
// ========================================

#include <stdio.h>
#include <lua.hpp>

int main() {

    // Lua の初期化関連
    lua_State* L = lua_open();
    luaL_openlibs(L);
    
    // Lua スクリプトを読み込む
    luaL_dofile(L, "script.lua");

    // スタックポインタを保存
    int top = lua_gettop(L);

    lua_getglobal(L, "TestLuaFunction1");   // 関数名
    lua_pushstring(L, "うっふーん");        // 第1引数
    lua_pushstring(L, "あっはーん");        // 第2引数

    // 関数実行(引数ふたつ、戻り値ひとつ)
    if (lua_pcall(L, 2, 1, NULL) != 0) {
        // 失敗したらエラー処理
        printf("error: %s\n", lua_tostring(L, -1));
    } else {
        // 成功したら、戻り値を取得
        int result = lua_tointeger(L, 1);
        printf("「こちら C です。戻り値は %d です」", result);
    }

    // スタックを元に戻す
    lua_settop(L, top);

    getchar();
    return 0;
}

実行すると、こんな感じになります。
今回は main() 関数にべた書きしましたが、たとえばこんな風に書けば(lua_State* がグローバルなのが気に食わないものの)、もうちょっと解りやすくなるかなぁ。

// ========================================
// あらかじめ、lua.hpp へのインクルードパス、
// lua5.1.lib へのライブラリパスを通しておこう
// ========================================

#include <stdio.h>
#include <lua.hpp>

lua_State* L;

int TestLuaFunction1(char* x1, char* x2) {
    int result;
    int top = lua_gettop(L);

    lua_getglobal(L, "TestLuaFunction1");   // 関数名
    lua_pushstring(L, x1);                  // 第1引数
    lua_pushstring(L, x2);                  // 第2引数

    if (lua_pcall(L, 2, 1, NULL) != 0) {
        printf("error: %s\n", lua_tostring(L, -1));
        result = -1;
    } else {
        result = lua_tointeger(L, 1);
    }

    return result;
}

int main() {
    // Lua の初期化関連
    L = lua_open();
    luaL_openlibs(L);
    
    // Lua スクリプトを読み込む
    luaL_dofile(L, "script.lua");

    // 関数呼び出し
    int result = TestLuaFunction1("(;゚д゚)ァ...", "(゚Д゚)ウボァー");

    // 戻り値表示
    printf("「こちら C です。戻り値は %d です」", result);

    getchar();
    return 0;
}



ほかにも、C++ 側の変数を Lua が書き換えたり、その逆もできたりするのですが、そうはいってもこの時点ではまだ Lua の良さが今ひとつ見えてきませんね。

C++ にバインドできるのはよいのですが、そのための準備が非常に面倒(Lua VM を生成したり、グルーコードを書いたり、データを受け渡すためにスタック操作をしたり……)。

そこらへんをなんとかする方法についても調べて書きたいのですが、そろそろ息切れし始めたので今日はこの辺で。

0 件のコメント:

コメントを投稿

ひとことどうぞφ(・ω・,,)