ADVENT CALENDAR 2019

初めての組み込みスクリプティング(C++ / Lua 基礎編)

By 檸檬茶(Lemon TEA)

最近,自作のゲームにスクリプトを埋め込んでみたいという思いでLuaに手を出してみました. スクリプト言語の一つであるLuaは,C/C++と特に相性が良く,割と簡単にスクリプトを組み込むことができます. 今回は,C++で作成したプログラムにLuaを組み込む方法を雑に紹介していきます.

実行環境は以下の通りです.

  • MacOS Catalina 10.15.1
  • GNU g++ 11.0.0
  • Lua 5.3.5

今回の記事の作成にあたり,以下の文献を参考にしました.

事前準備

其の壱 : ヘッダのインクルード

C++にLuaを組み込むとき,以下のヘッダをインクルードします.

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

様々な技術ブログで紹介されているように,インクルードするヘッダはlua.hppです. 間違ってもlua.hをインクルードしないでください.

其の弐 : Luaステートの作成

Luaを組み込むからには,Luaスクリプトを解釈するモノが必要になります. それがLuaステートになります. Luaステートに作成したコードを投げると,スクリプトを実行したり,変数や関数を保持したりと,いろんなことをよろしくやってくれます. Luaステートは以下のようにして作成します. 変数名は任意ですが,以下よりstateという名前で説明していきます.

lua_State* state = luaL_newstate();

なお,作成したステートは破棄しないと,いつまで経ってもメモリ上に残ることになります. Luaステートを破棄する場合は以下のようにします.

lua_close(state);

其の参 : 標準ライブラリの読み込み

ヘッダをインクルードしてステートの作成さえすれば,Luaのスクリプトを実行することができます. しかし,Luaスクリプトでより便利な機能を使うには,標準ライブラリを明示的に読み込む必要があります. 以下のようにして読み込むことができます.

luaL_openlibs(state);

このようにしないと,例えばデバッグのために標準出力をしようとしたときにattempt to call a nil value (global 'print')というエラーが出ます. (自分はそれで6時間ぐらいハマってました.) C++にLuaを組み込むときは,絶対にこの操作を行いましょう.

以上をまとめると,Luaが組み込まれたC++の最小限プログラムは以下のようになります.

//////////////////////////////////////////////////
//
//    program0.cpp
//    Lua が組み込まれた C++ の最小限プログラム
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();

    // 明示的に標準ライブラリの読み込み
    luaL_openlibs(state);

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}

其の肆 : コンパイル

C++のプログラムを書いたら,実行するためにコンパイルしなければなりません. MacOSでgccコンパイラを使用する場合,Luaに関するライブラリをリンクするため,**-llua**をオプションに加えます.

g++ -O -o program program0.cpp -llua

先ほど示したC++ソースコードをこのコマンドでコンパイルして実行すると,エラー含め何も起こらずにプログラムが終了すると思います. もちろん,Luaがインストールされていない環境ではライブラリがないぞとか起こられると思うので,Homebrewか何かでインストールしてから,再度この操作を行なってください. このソースコードを雛形とし,C++/Luaによるスクリプティングのサンプルを示していこうと思います.

実際に組み込んでみる

其の壱 : スクリプトファイルを丸々実行する

スクリプトファイルを実行するには,まずはファイルを読み込む必要があります. luaL_loadfile関数で,スクリプトファイルを読み込むことができます. 引数には,左からLuaステート・スクリプトファイルの名前を指定します.

luaL_loadfile(state, "script.lua");

次に,ステートに読み込んだLuaスクリプトを実行します. lua_pcall関数で,スクリプトを丸々実行することができます. 引数には,左からLuaステート・Lua内の関数の引数の数・Lua内の関数の戻り値の数・エラー関数の位置を指定します. 第2・3引数は,スクリプトで定義された関数をC++側で使用するために使います. 第4引数は独自のエラーメッセージを使用するために使います. 丸々実行するだけなら,第2引数以降は0を指定すればおkです.

lua_pcall(state, 0, 0, 0);

以上をまとめると,Luaスクリプトを実行するだけのプログラムは以下のようになります.

//////////////////////////////////////////////////
//
//    program1.cpp
//    Lua スクリプトを丸々実行する
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ファイルの読み込み
    luaL_loadfile(state, argv[1]);

    // Lua スクリプトの実行
    lua_pcall(state, 0, 0, 0);

    // スクリプト実行時に発生したエラーに対して処理をする場合,
    // 以下のように書き換える
    /*
    if(lua_pcall(state, 0, 0, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }
    */

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}

ここで,コメントアウトされたif文に注目してください. スクリプト実行時に発生したエラーメッセージは「スタック」と呼ばれるものに格納されます. エラーメッセージを表示するには,このスタックの一番上の値を取得すればおkです. 実はこのスタックは,エラーメッセージの表示だけでなく,グローバル変数や関数,テーブルの設定などにも使用されます. スタックの詳細については次節で紹介します.

program1.cppをコンパイルし,以下のスクリプトファイルをコマンドライン引数に指定して実行してみてください.

-- Hello world を出力するだけ
print("Hello world from lua!")
g++ -std=c++17 -O -o program program1.cpp -llua
./program script1.lua

何もなければ Hello world が出力されるはずです.

Tips : luaL_dofile について

Luaスクリプトの読み込みと実行を同時に行ってくれるluaL_dofile関数があります.ただスクリプトを実行するためだけにluaL_loadfileしてlua_pcallするのが面倒臭いという人は,是非使ってみてください.

其の弐 : スタック

スタックは,データを「後入れ先出し」するデータ構造です. 要するに,値を上に上に乗せていき,積み上がったデータを上から取り出すという構造です. イメージとしては下図のようになります. ACAC-2019-12-06(1).png

この構造を使うことで,LuaスクリプトとC++で値の受け渡しができます. スタックは,Luaステートごとに用意されています. では実際に,Luaステートのスタックに値を出し入れしてみましょう.

まず,Luaステートのスタックに値を積むためには,主にlua_push****系の関数を使います. 格納する値の型によってこれらの関数を使い分ける訳ですが,スタックに格納できる値の型は9つあります.

定義済みマクロ スタックに積む時の関数
nil NULL LUA_TNIL lua_pushnil
真偽値 true / false LUA_TBOOLEAN lua_pushboolean
軽量ユーザーデータ voidポインタ LUA_TLIGHTUSERDATA lua_pushlightuserdata
数値 基本はdouble型 LUA_TNUMBER lua_pushnumber
文字列 const char* 型 LUA_TSTRING lua_pushstring
テーブル C++でいう構造体のようなもの LUA_TTABLE lua_settable
関数 int (*)(lua_State*) 型 LUA_TFUNCTION lua_pushfunction
ユーザーデータ メモリブロック LUA_TUSERDATA lua_pushuserdata
スレッド(コルーチン) コルーチンの実行に使っているLuaステート LUA_TTHREAD lua_pushthread

スタックに積まれている値は,ほとんどはnil,真偽値,数値,文字列,テーブル,関数です. サンプルソースコードではnil,真偽値,数値,文字列を格納していきます.

次に,スタックに積まれている値およびその型を調べます. スタック内のデータの型を調べるには,lua_type関数を利用します.

lua_type(state, i)

stateには現在使用しているLuaステート,iにはスタックのインデックス番号を指定します. インデックス番号には正の整数と負の整数を指定することができます. 正の整数を指定した場合,スタックの下から数えてi番目のデータにアクセスすることができます. 逆に負の整数を指定した場合,スタックの上から数えてi番目のデータにアクセスすることができます. すなわち,スタックにデータが5つ積まれていた場合,「-1番目のデータ」と「5番目のデータ」は同じデータを指していることになります.

ACAC-2019-12-06(2).png

また,スタックに積まれている値を調べるには,lua_to****系の関数を使います.

// Ex.) 数値を取得する関数
lua_tonumber(state, i)

先ほどと同じように,stateには現在使用しているLuaステート,iにはスタックのインデックス番号を指定します. lua_typeで取得したデータ型によって関数を使い分けてください.

これらを踏まえると,スタックの内容を表示する関数は以下のように実装されます. コピペしてそのままコンパイルできるようにするため,サンプルソースコードではメイン関数も示しています.

//////////////////////////////////////////////////
//
//    program2.cpp
//    Lua ステートが持つスタックに値を push したり
//    pop したりする
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

// スタックの内容を表示する関数
void print_stack(lua_State* state)
{
    printf("\n==================================================\n");

    // 最後に格納した値のインデックスを取得・表示
    // このインデックスがスタックに積まれた値の数である.
    int stack_amount = lua_gettop(state);
    printf("%d values are in this stack.", stack_amount);

    printf("\n--------------------------------------------------\n");

    // スタックの内容の表示
    for(int i = stack_amount; i > 0; --i)
    {
        // 値が格納されているインデックスの表示
        printf("%03d(%04d) : ", i, i - stack_amount - 1);

        // 型名を取得して条件分岐
        switch(lua_type(state, i))
        {
        case LUA_TNIL:
            printf("NIL           : \n");
            break;
        case LUA_TBOOLEAN:
            // lua_toboolean でスタックの値を bool 型に変換
            printf("BOOLEAN       : %s\n", lua_toboolean(state, i) ? "true" : "false");
            break;
        case LUA_TLIGHTUSERDATA:
            printf("LIGHTUSERDATA : \n");
            break;
        case LUA_TNUMBER:
            // lua_tonumber でスタックの値を double 型に変換
            printf("NUMBER        : %f\n", lua_tonumber(state, i));
            break;
        case LUA_TSTRING:
            // lua_tostring でスタックの値を string 型に変換
            printf("STRING        : %s\n", lua_tostring(state, i));
            break;
        case LUA_TTABLE:
            printf("TABLE         : \n");
            break;
        case LUA_TFUNCTION:
            printf("FUNCTION      : \n");
            break;
        case LUA_TUSERDATA:
            printf("USERDATA      : \n");
            break;
        case LUA_TTHREAD:
            printf("THREAD        : \n");
            break;
        }
    }

    printf("==================================================\n");
}

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ステートのスタックに値を積む
    lua_pushnil(state);
    lua_pushnumber(state, 2.71);
    lua_pushnumber(state, 3.14);
    lua_pushstring(state, "Lua");
    lua_pushboolean(state, true);

    // スタックの中身を出力
    print_stack(state);
    printf("\n");

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}

このプログラムをコンパイルして実行したとき,以下のような出力結果が得られるはずです.

==================================================
5 values are in this stack.
--------------------------------------------------
005(-001) : BOOLEAN       : true
004(-002) : STRING        : Lua
003(-003) : NUMBER        : 3.140000
002(-004) : NUMBER        : 2.710000
001(-005) : NIL           : 
==================================================

其の参 : グローバル変数の値の受け渡し

前節で説明したスタックを用いて,LuaとC++の間で値のやり取りをしてみましょう.

Luaで定義された変数をC++で取得する

Luaスクリプト内のグローバル変数の値をC++で取得するときは,lua_getglobal関数を使います.

lua_getglobal(state, name)

stateには現在使用しているLuaステート,nameには値を取得したい変数の名前を指定します. この関数を実行すると,nameに指定された変数の値がスタックの一番上に積まれます. あとはその値をlua_to****系関数で読み取れば取得完了です. 実際これだけです.

以下がC++/Luaのサンプルソースコードです.

//////////////////////////////////////////////////
//
//    program3-1.cpp
//    Lua で定義されたグローバル変数を C++ で取得する
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ファイルの読み込み
    luaL_loadfile(state, argv[1]);

    // Lua スクリプトの実行
    if(lua_pcall(state, 0, 0, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }

    // グローバル変数を読み出してプリント
    lua_getglobal(state, "string");
    printf("string = %s\n", lua_tostring(state, -1));
    lua_getglobal(state, "number");
    printf("number = %f\n", lua_tonumber(state, -1));
    lua_getglobal(state, "boolean");
    printf("boolean = %s\n", lua_toboolean(state, -1) ? "true" : "false");

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}
-- C++ に渡す変数を Lua で定義する
number = 3.1415925
string = "This is a lua script."
boolean = true

これをprogram1.cppをコンパイル&実行する体で,同じ様にprogram3-1.cppをコンパイル&実行してみてください. 以下の内容が出力されるはずです.

string = This is a lua script.
number = 3.141592
boolean = true

C++で設定された変数をLuaで使用する

Luaスクリプト内で使用するグローバル変数の値をC++で設定するときは,lua_setglobal関数を使います.

lua_setglobal(state, name)

この関数を実行すると,スタックの一番上に格納された値が,nameに指定された名前の変数に格納されます. すなわち,この関数を実行する直前でlua_push****系の関数を使うと,任意の値をグローバル変数に格納することができます. これだけです.

以下がC++/Luaのサンプルソースコードです.

//////////////////////////////////////////////////
//
//    program3-2.cpp
//    C++ で設定されたグローバル変数を Lua で使用する
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ファイルの読み込み
    luaL_loadfile(state, argv[1]);

    // 設定する値をスタックに積んで,積んだ値をグローバル変数に代入する
    lua_pushnumber(state, 56);
    lua_setglobal(state, "x");
    lua_pushnumber(state, 7);
    lua_setglobal(state, "y");

    // Lua スクリプトの実行
    if(lua_pcall(state, 0, 0, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}
-- C++ で定義されたグローバル変数を使って何かする
print(x .. " plus " .. y .. " is ".. (x + y) .. ".")
print(x .. " minus " .. y .. " is ".. (x - y) .. ".")
print(x .. " multiplied by " .. y .. " is ".. (x * y) .. ".")
print(x .. " divided by " .. y .. " is ".. (x / y) .. ".")

先ほどと同じ様にprogram3-2.cppをコンパイル&実行してみてください. 以下の内容が出力されるはずです.

56.0 plus 7.0 is 63.0.
56.0 minus 7.0 is 49.0.
56.0 multiplied by 7.0 is 392.0.
56.0 divided by 7.0 is 8.0.

其の肆 : 関数の受け渡し

Luaで定義された関数をC++で使用する

Luaで定義された関数をC++で取得するときも,変数を取得する時と同じ様にlua_getglobal関数を用いて取得することができます. しかし取得した関数をC++で実行するためにはもう一手間必要になります. それが引数の設定です. lua_push****系の関数を使用すると,スタックに積まれた順に第一引数,第二引数,……を指定することができます.

// Lua スクリプトで定義された関数をスタックに積む
lua_getglobal(state, "arithmetic");

// 引数とする値をスタックに積む
lua_pushnumber(state, 64);
lua_pushnumber(state, 36);

これを行ったらいよいよ実行します. 取得した関数を実行する関数はlua_pcallです. ただしその壱で説明した様に,第二引数に引数の数,第三引数に戻り値の数を指定して実行します. 今回のスクリプトでは,引数が2つ,戻り値が4つの関数を定義しているため,以下の様にして実行します.

lua_pcall(state, 2, 4, 0)

この関数を実行すると,第一戻り値,第二戻り値,……の順に戻り値がスタックに積まれていきます. あとはスタックに積まれた戻り値をlua_to****系の関数で取得すればおkです.

以下がC++/Luaのサンプルソースコードです.

//////////////////////////////////////////////////
//
//    program4-1.cpp
//    Lua で定義された関数を C++ で使用する
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ファイルの読み込み
    luaL_loadfile(state, argv[1]);

    // まずは Lua スクリプトを引数なしで実行
    if(lua_pcall(state, 0, 0, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }

    // Lua スクリプトで定義された関数をスタックに積む
    lua_getglobal(state, "arithmetic");

    // 引数とする値をスタックに積む
    lua_pushnumber(state, 64);
    lua_pushnumber(state, 36);

    // 実行する関数をスタックのトップに持ってきた上で
    // もう一度スクリプトを実行する
    if(lua_pcall(state, 2, 4, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }

    // 戻り値は Lua スクリプトで return された順にスタックに積まれる
    printf("sum = %lf\n", lua_tonumber(state, -4));
    printf("dif = %lf\n", lua_tonumber(state, -3));
    printf("mul = %lf\n", lua_tonumber(state, -2));
    printf("div = %lf\n", lua_tonumber(state, -1));

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}
-- 加減乗除を行って4つの値を返す関数
function arithmetic (x, y)
    return x + y, x - y, x * y, x / y;
end

program1.cppをコンパイル&実行する体で,program4-1.cppをコンパイル&実行してみてください. 以下の内容が出力されるはずです.

sum = 100.000000
dif = 28.000000
mul = 2304.000000
div = 1.777778

C++で定義された関数をLuaで使用する

C++で定義された関数をLuaで使用するときも,変数を設定する時と同じ様にlua_setglobal関数を用いて取得することができます. ただし,いかなる関数でもスタックに積んでlua_setglobalすればLuaで実行することができるというわけではありません. Luaで使用できる関数をC++で定義するときは,決まった形式で定義する必要があります. 以下のソースコードが,Luaで使用できる最小の関数になります.

int FuncC(lua_State *L)
{
    return 0;
}

すなわち,**引数がlua_Stateポインタであり,戻り値がintである関数のみをスタックに積むことができます.**ただし,**この関数の戻り値には,Luaで使用される関数の戻り値の数を指定します.**例えば,script4-1.luaで定義されたarithmetic関数をC++側で定義すると,今回のサンプルソースコードの様になります.

以下がそのサンプルソースコードになります.

//////////////////////////////////////////////////
//
//    program4-2.cpp
//    C++ で定義された関数を Lua で使用する
//

#include <cmath>

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

// Lua が理解できるような形で関数を定義する
// Lua の組み込みにおいて, lua_State* を引数にとり,
// Lua スクリプトでの戻り値の数を返す↓この関数を「グルー(Glue)関数」と呼ぶ 
int arithmetic(lua_State* state)
{
    // スタックから引数を受け取る
    double x = lua_tonumber(state, -2);
    double y = lua_tonumber(state, -1);

    // スタックの削除
    lua_pop(state, -1);

    // 和・差・積・商を戻り値としてスタックに積む
    lua_pushnumber(state, x + y);
    lua_pushnumber(state, x - y);
    lua_pushnumber(state, x * y);
    lua_pushnumber(state, x / y);

    // 最後に戻り値の数を返す
    return 4;
}

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ファイルの読み込み
    luaL_loadfile(state, argv[1]);

    // C++ で定義された関数をスタックに積み,
    // 積んだ関数をグローバル変数として設定する
    lua_pushcfunction(state, arithmetic);
    lua_setglobal(state, "arithmetic");
    
    // Lua スクリプトの実行
    if(lua_pcall(state, 0, 0, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}
-- ここで使用する関数は C++ で定義されているとする
x = 56.0
y = 7.0
sum, dif, mul, div = arithmetic(x, y)
print(x .. " plus " .. y .. " is ".. sum .. ".")
print(x .. " minus " .. y .. " is ".. dif .. ".")
print(x .. " multiplied by " .. y .. " is ".. mul .. ".")
print(x .. " divided by " .. y .. " is ".. div .. ".")

先ほどと同じ様にprogram4-2.cppをコンパイル&実行してみてください. 以下の内容が出力されるはずです.

56.0 plus 7.0 is 63.0.
56.0 minus 7.0 is 49.0.
56.0 multiplied by 7.0 is 392.0.
56.0 divided by 7.0 is 8.0.

其の伍 : テーブルの受け渡し

Luaで定義されたテーブルをC++で取得する

Luaにおける「テーブル」と呼ばれるものは,C/C++で言うところの構造体にあたり,異なるデータ型をまとめて扱うことができます. Luaで定義されたテーブルをC++で取得するときも,変数や関数を取得する時と同じ様にlua_getglobal関数を用いて取得することができます. ただし,テーブルに格納された値を取得するには別の関数が必要になります. それがlua_getfield関数です.

lua_getfield(state, table_pos, name);

この関数を使用する際,第一引数に現在使用しているLuaステート,第二引数にテーブルが格納されているスタック上の位置,第三引数にテーブル上にある,いわゆるメンバー変数の名前を指定します. この関数を実行すると,table_posに指定されたテーブルの,nameに指定されたメンバ変数の値がスタックに積まれます. あとはその値をlua_to****系関数で読み取れば取得完了です.

以上を踏まえると,Luaで定義されたテーブルをC++で取得するプログラムは,以下のサンプルソースコードの様に実装されます.

//////////////////////////////////////////////////
//
//    program5-1.cpp
//    Lua で定義されたテーブルの内容を C++ で読み出す
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ファイルを読み込んで実行
    luaL_loadfile(state, argv[1]);
    if(lua_pcall(state, 0, 0, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }

    // 読み込んだファイルからテーブルの情報を読みだす
    lua_getglobal(state, "table");
    int table_pos = lua_gettop(state);

    // テーブルに格納された変数の情報を読み出す
    lua_getfield(state, table_pos, "number");
    printf("number = %lf\n", lua_tonumber(state, -1));
    lua_getfield(state, table_pos, "string");
    printf("string = %s\n", lua_tostring(state, -1));
    lua_getfield(state, table_pos, "boolean");
    printf("boolean = %s\n", lua_toboolean(state, -1) ? "true" : "false");

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}
-- Lua でテーブルを定義する
table =
{
    number = 3.1415925,
    string = "This is a lua script.",
    boolean = true,
}
-- Lua でテーブルを定義する
table =
{
    number = 3.1415925,
    string = "This is a lua script.",
    boolean = true,
}

これも,program1.cppをコンパイル&実行する体で,program5-1.cppをコンパイル,実行してみてください. 以下の内容が出力されるはずです.

number = 3.141592
string = This is a lua script.
boolean = true

C++で設定されたテーブルをLuaで使用する

C++で設定されたテーブルをLuaで使用するときも,変数や関数を設定する時と同じ様にlua_setglobal関数を用いて取得することができます. ところで,C++側でテーブルを定義するときは,やはりスタックを利用します. C++でテーブルを定義する手順は以下の様になります.

其の壱 : テーブルを新規作成する

手始めに,Luaスクリプトに渡すためのテーブルを新しく作成します. lua_newtable関数を使うことで,テーブルを新規作成することができます.

lua_newtable(state);

stateには,現在使用しているLuaステートを指定します. この関数を実行すると,空っぽのテーブルがスタックに積まれます.

其の弐 : フィールドを追加する

空っぽのテーブルにフィールドを追加するには,lua_setfield関数を使います.

 lua_setfield(state, table_pos, name);

stateには,現在使用しているLuaステート,table_idxにはテーブルが格納されているスタック上の位置,nameには追加するフィールドの名前を指定します. この関数を実行すると,スタックの一番上にある値がnameに指定された名前の変数に格納され,table_posに指定されたテーブルにフィールドとして追加されます. すなわち,Luaスクリプト内のグローバル変数や関数を設定する時と同じ様に,この関数を実行する直前でlua_push****系の関数を使うと,任意の値をグローバル変数に格納することができます.

lua_pushstring(state, "This is a lua script.");
lua_setfield(state, -2, "string");

追加したいフィールドの数だけ,この操作を繰り返します.

其の参 : 作成したテーブルをグローバル変数として設定

作成したテーブルをlua_setglobalしてテーブルの設定は完了になります.

以上を踏まえて,C++/Luaのサンプルソースコードを示します.

//////////////////////////////////////////////////
//
//    program5-2.cpp
//    C++ で設定されたテーブルを Lua で使用する
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ファイルを読み込み
    luaL_loadfile(state, argv[1]);

    // テーブルの作成
    lua_newtable(state);
    lua_pushstring(state, "This is a lua script.");
    lua_setfield(state, -2, "string");
    lua_pushnumber(state, 3.14159265);
    lua_setfield(state, -2, "number");
    lua_pushboolean(state, true);
    lua_setfield(state, -2, "boolean");
    lua_setglobal(state, "table");

    // スクリプトの実行
    if(lua_pcall(state, 0, 0, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}
-- C++ で設定されたテーブルの内容を出力する
print("string = " .. table.string)
print("number = " .. table.number)
print("boolean = " .. table.boolean)

先ほどと同じ様にprogram5-2.cppをコンパイル,実行してみてください. 以下の内容が出力されるはずです.

string = This is a lua script.
number = 3.14159265
boolean = true

其の陸 : コルーチン

コルーチンは「関数の途中で処理を中断し,再度呼び出した時に続きから処理を開始する」と言う仕組みです. コルーチンを使うと,状態遷移の実装をより少ない記述で行うことができます. 今回はLua側でコルーチンを定義し,C++側でそれを呼び出していきます.

Luaスクリプト内でコルーチンを定義する場合は以下の様に記述します.

-- コルーチンを用いた関数
function co()
    coroutine.yield("そこは広場だった")
    coroutine.yield("小さな滑り台があった")
    coroutine.yield("昔ここでよく遊んだことを思い出した")
end

コルーチンの役割を果たすco関数の中に,coroutine.yieldがいくつかあります. このcoroutine.yieldの部分で処理が中断されます. このとき,coroutine.yieldの引数に何かしらの値を指定すると,引数に指定した値をコルーチン関数の戻り値とすることができます. もちろん,何も指定しなくてもおkです.

次に,C++側での実装を見ていきましょう. コルーチンを動かすためには,コルーチンのためのスレッドを新たに作る必要があります. lua_newthread関数でスレッドを新規作成することができます.

lua_State* coroutine = lua_newthread(state);

次に,lua_getglobal関数でコルーチン関数を指定します. ここで注意して欲しいのは,第一引数に指定するのは lua_newthread関数によって新たに作成したLuaステートです.

lua_getglobal(coroutine, "co")

次に,いよいよコルーチンを走らせる訳ですが,コルーチン関数を呼び出す関数はlua_pcall関数ではなく,lua_resume関数です.

lua_resume(state, form, narg)

stateにはlua_newthread関数によって新たに作成したLuaステート,formにはstateのコルーチンの処理を再開させる側のコルーチン,nargにはLuaスクリプトに渡す引数の数を指定します. 特別な用事がない限り,formにはnullptrを指定すればおkです. この関数を実行した時の戻り値はスタックに積まれているため,Luaで定義した関数をC++で実行する時と同じ様に,スタックから戻り値を取得することができます.

以上を踏まえると,コルーチンを利用するプログラムは以下のサンプルソースコードの様になります.

//////////////////////////////////////////////////
//
//    program6.cpp
//    コルーチンを使う
//

#include <lua/lua.hpp>
#include <lua/lualib.h>
#include <lua/lauxlib.h>

using namespace std;

int main(int argc, char** argv)
{
    // Lua ステートの作成
    lua_State* state = luaL_newstate();
    luaL_openlibs(state);

    // Lua ファイルの読み込み
    luaL_loadfile(state, argv[1]);

    // まずは Lua スクリプトを引数なしで実行
    if(lua_pcall(state, 0, 0, 0))
    {
        // エラーを出力して終了
        printf("%s\n", lua_tostring(state, -1));
        lua_close(state);
        return -1;
    }

    // スレッドを生成する
    lua_State* coroutine = lua_newthread(state);

    // コルーチンステート内にあるコルーチン関数を指定する
    lua_getglobal(coroutine, "co");
    
    // コルーチンの実行が終わるまでループ
    while(lua_resume(coroutine, nullptr, 0))
        printf("%s\n", lua_tostring(coroutine, -1));

    // Lua ステートの破棄
    lua_close(state);

    return 0;
}
-- コルーチンを用いた関数
function co()
    coroutine.yield("そこは広場だった")
    coroutine.yield("小さな滑り台があった")
    coroutine.yield("昔ここでよく遊んだことを思い出した")
end

これも,program1.cppをコンパイル&実行する体で,program6.cppをコンパイル,実行してみてください. 以下の内容が出力されるはずです.

そこは広場だった
小さな滑り台があった
昔ここでよく遊んだことを思い出した

終わりに / 次回予告

単一の言語で書かれたプログラムに異なる言語を組み込むのは,少しハードルが高いと思う人もいるかもしれませんが,C++ / Luaで組み込みスクリプティングを初めてみると,案外そこまで難しいことはありません. C++でゲームを作っていて,かつLuaを組み込んだことがないと言う方は,是非一度でもLuaスクリプティングをしてみてはいかがでしょうか.

今回作成したサンプルソースコードはgithub上で公開しています. 詳しくは下のリンク先のリポジトリの「Day6」をご参照ください. https://github.com/GCLemon/ACAC2019

次回の9日目の記事では,実際にLuaスクリプトをAltseed製のゲームに組み込むサンプルを示していきます. どうぞお楽しみください.

SHARE THIS POST