Lua

LuaのC++組み込み方自分用まとめ

More than 3 years have passed since last update.

前にLuaを入門Luaプログラミングで勉強してたんですが、Luaの文法は分かっても肝心の組み込み方やコルーチン・メタテーブル等はサラッとしか書いておらず、結局どう使えばいいのやらサッパリ分からなくて挫折してました。

最近、Programming in Luaという公式の解説書を読んで、勉強し直してます。

さすが公式というだけあって、情報量が半端ない。

C++組み込み部分だけでも、自分用に備忘録を作っておきます。

ちなみに、ライブラリのリンクが一番手間取ったのですが、今でもよく分かってないので書きません。

Makefileでビルドしたのをリンクしても、↓のようなエラーが出てリンクできません。


ignoring file.../liblua.a, file was built for archive which is not the architecture being linked (x86_64): .../liblua.a


結局ソースファイルを全部プロジェクトに埋め込んでコンパイルしてる始末です。


Lua組み込みの概要


Lua組み込みの準備

インクルードするヘッダファイルは、基本的には以下の3つ。

※ lua.hでは無くlua.hppをインクルードすることに注意。

間違えるとエラーが出て動きません。

#include "lua.hpp"

#include "lauxlib.h"
#include "lualib.h"

LuaをC++から呼び出す前に、Lua_Stateの新規作成が必要。

以降、Lua関連関数の呼び出し時には常に作成したLua_Stateを引数に渡す。

lua_State *L = luaL_newstate();

作成したLua_Stateは、初期状態では標準ライブラリを読み込んでいないので、明示的に読み込みを行う。

luaL_openlibs(L);


Luaファイルの読み込み・実行

Luaファイルを読み込んで実行する場合は、luaL_loadfileを使う。

※ 引数のファイル名は、const char*型なので、ファイル名がstring型の場合はc_str()で変換が必要。

luaL_loadfile(L, "test.lua");

ファイルを読みこんだだけではスクリプトは実行されない。lua_pcallを使って、明示的に実行する必要がある。

※ 2番目以降の引数は、引数の数・戻り値の数・エラー関数の位置。ファイルをまるごと実行する場合はどれも0。

lua_pcall(L, 0, 0, 0);

もしスクリプト実行時にエラーが起きても、このままではエラーが出力されず、プログラムの実行も継続される。

lua_pcallはスクリプト実行時に0以外の値を返すので、それをキャッチしてエラー処理を行う。

エラー処理ではスタックを使っているが、これについては後で説明する。

if(lua_pcall(L,0,0,0) != 0) {

print(lua_tostring(L, -1));
lua_close(L);
exit(EXIT_FAILURE);
}


スタック

C++とLuaの間では、直接互いのデータにアクセスすることはできません。

例えば、Luaのグローバル変数aの値をC++から直接取得したり変更したりすることはできませんし、逆もしかりです。

その代わりに、スタックという仕組みを使って互いにデータをやりとりします。

例えば、Luaのグローバル変数aをC++で取得する場合、以下のようなコードを記述します。

lua_getglobal(L, "a");

int a = lua_tonumber(L, -1);

lua_getglobalは、引数で指定された名前のグローバル変数の値を取得し、スタックのトップに追加します。

変数aの値が100だった場合、スタックは以下の図のようになります。

index
index(逆順)

1
-1
100

次に、lua_tonumberで-1番目(上から数えて1番目)の値を取得しています。つまり、上の図のindex:1の値100を取得して、C++の変数aに代入しています。

このように、C++とLuaの間では、常にスタックを仲介してデータをやりとりします。

こうすることで、C++とLuaのデータ型の扱いの差異・メモリ管理の差異を気にすることなくデータをやりとりすることができます。


C++からLuaへのアクセス


Luaのグローバル変数の取得

C++からLuaのグローバル変数の値を取得する場合はlua_getglobalとlua_toXXXXを使用します。

例えば、lua上で以下のようなグローバル変数が定義されているとします。

num  = 1.23

str = "text"
boo = true
int = 10

これらを取得するC++のコードは、以下のようになります。

lua_getglobal(L, "num");

double num = lua_tonumber(L, -1);
lua_getglobal(L, "str");
const char *str = lua_tostring(L, -1);
lua_getglobal(L, "boo");
int boo = lua_toboolean(L, -1);
lua_getglobal(L, "integer");
int integer = lua_tointeger(L, -1);

lua_toXXXは型チェックを行いません。型チェックを行い、エラーのときはメッセージを出力してプログラムを終了したい場合は、lua_toXXXをlua_checkXXXに変更します。

lua_getglobal(L, "num");

int num = lua_checknumber(L, -1);
lua_getglobal(L, "str");
int str = lua_checkstring(L, -1);
lua_getglobal(L, "bool");
int boo = lua_checkboolean(L, -1);
lua_getglobal(L, "int");
int integer = lua_checkinteger(L, -1);


テーブルの要素の取得

テーブルの要素を取得する場合は、もう少し面倒な作業が必要になります。

例えば、lua上で以下のようなグローバル変数が定義されているとします。

background = { r = 255, g = 122, b = 0}

これらを取得するC++のコードは、以下のようになります。

float r,g,b;

lua_getglobal(L, "background");
lua_getfield(L, -1, "r");
lua_getfield(L, -2, "g");
lua_getfield(L, -3, "b");
r = lua_checknumber(L, -3);
g = lua_checknumber(L, -2);
b = lua_checknumber(L, -1);

このコードについては、説明が必要でしょう。

まず、2行目のlua_getglobalで、テーブルをスタックのトップに追加しています。

index
index(逆順)

1
-1
{r=255,g=122,b=0}

その後、lua_getfieldで、第二引数のインデックスに存在するテーブルから第三引数の名前の要素を取得し、スタックのトップに追加しています。

つまり、lua_getfield(L, -1, "r");の実行後にはスタックは以下のようになります。

index
index(逆順)

2
-1
255

1
-2
{r=255,g=122,b=0}

このとき、テーブルのindexが-1から-2に変わっています。

そのため、次のlua_getfield(L, -2, "g")では、第二引数が-2になっています。この関数を実行した後は、スタックは以下のようになります。

index
index(逆順)

3
-1
122

2
-2
255

1
-3
{r=255,g=122,b=0}

次に、lua_getfield(L, -3, "b")を実行すると、スタックは以下のようになります。

index
index(逆順)

4
-1
0

3
-2
122

2
-3
255

1
-4
{r=255,g=122,b=0}

最後の三行で、lua_checknumberを使用して、上のスタックから要素r,g,bに対応する値を取得しています。


Lua関数の呼び出し

Luaでは、関数もオブジェクトの一種とみなされ、内部的にはグローバル変数に保管されています。

そのため、他の変数と同様、lua_getglobalを用いてLua関数を取得できます。


引数・戻り値が無い場合

まず、引数・戻り値の無いLua関数の呼び出し方を説明します。以下のようなLua関数があるとします。

function hello()

print("Hello!")
end

この関数を呼び出すC++のコードは、以下のようになります。

lua_getglobal(L, "hello");

lua_pcall(L, 0, 0, 0);

まず、lua_getglobalでLua関数をスタックのトップに追加し、その後のlua_pcallでスタックトップのLua関数を実行しています。

エラーチェックを行う場合は、以下のようなコードになります。

lua_getglobal(L, "hello");

if(lua_pcall(L,0,0,0) != 0) {
print(lua_tostring(L, -1));
lua_close(L);
exit(EXIT_FAILURE);
}


引数・戻り値がある場合

次に、引数・戻り値があるLua関数の呼び出し方を説明します。以下のようなLua関数があるとします。

function add(a,b)

return a+b
end

この関数を呼び出すC++のコードは、以下のように

なります。

lua_getglobal(L, "add");

lua_pushnumber(L, 50);
lua_pushnumber(L, 100);
lua_pcall(L, 2, 1, 0);
int aplusb = lua_checknumber(L, -1);

まず、lua_getglobalでLua関数をスタックのトップに追加しています。

index
index(逆順)

1
-1
function add

次に、lua_pushnumberで第一引数をスタックのトップに追加しています。

index
index(逆順)

2
-1
50

1
-2
function add

次に、lua_pushnumberで第二引数をスタックのトップに追加しています。

index
index(逆順)

3
-1
100

2
-2
50

1
-3
function add

その後、lua_pcallで関数を呼び出します。第二引数で引数の数を2個に指定しているため、スタックの上から二つを引数とし、スタックの上から3番目の関数を呼び出しています。

lua_pcallの第三引数で戻り値の数を1個に指定しているので、関数呼び出しが終わった後はスタックのトップに関数の実行結果が追加された状態になります。(引数・関数はスタックから削除されます)

index
index(逆順)

1
-1
150

最後に、lua_checknumberを使ってスタックのトップから関数の実行結果を取得し、変数aplusbに代入しています。


LuaからC++へのアクセス

スタックを明示的に操作する関数は、C++側からしか呼び出すことができません。

そのため、LuaからC++にアクセスする場合、C++側からLuaに要素を渡すか、C++側にLuaから呼び出される関数を用意しておいてLuaから呼び出すという形になります。


C++からLuaのグローバル変数の値をセットする

C++からLuaのグローバル変数の値をセットする場合、Luaのグローバル変数の取得とほぼ逆の手順になります。

Luaのグローバル変数aに100をセットするC++コードは、以下のようになります。

lua_pushnumber(L, 100);

lua_setglobal(L, "a");

まず、lua_pushnumberでスタックのトップに値100を追加します。

その後にlua_setglobalで、スタックのトップの値をグローバル変数aにセットしています。

その他の型についても、同様の手順でセットが行えます。

lua_pushnumber(L, 1.23);

lua_setglobal(L, "num");

lua_pushstring(L, "text");
lua_setglobal(L, "str");

lua_pushboolean(L, 1);
lua_setglobal(L, "boo");

lua_pushinteger(L, 100);
lua_setglobal(L, "integer");


テーブルをセットする

Luaにテーブル型のグローバル変数をセットする場合は、もう少し面倒な作業が必要になります。

ここでは、要素r,g,bを持つテーブル型のグローバル変数REDをLuaに追加するコードを記載します。

// テーブルを作成

lua_newtable(L);
// テーブルに要素rを追加
lua_pushnumber(L, 255);
lua_setfield(L, -2, "r");
// テーブルに要素gを追加
lua_pushnumber(L, 0);
lua_setfield(L, -2, "g");
// テーブルに要素bを追加
lua_pushnumber(L, 0);
lua_setfield(L, -2, "b");
// テーブルをグローバル変数REDとしてLuaに追加
lua_setglobal(L, "RED");

まず、最初のlua_newtableで空のテーブルをスタックのトップに追加しています。

index
index(逆順)

1
-1
{}

次に、lua_pushnumberで値255をスタックのトップに追加しています。

index
index(逆順)

2
-1
255

1
-2
{}

次のlua_setfieldで、第二引数-2のテーブルに、スタックのトップの値255を第三引数で指定した要素rの値として追加しています。

その際、スタックのトップはポップされます。

index
index(逆順)

1
-2
{r=255}

さらに、lua_pushnumberで値0をスタックのトップに追加しています。

index
index(逆順)

2
-1
0

1
-2
{r=255}

次のlua_setfieldで、第二引数-2のテーブルに、スタックのトップの値255を第三引数で指定した要素gの値として追加しています。その際、スタックのトップはポップされます。

index
index(逆順)

1
-2
{r=255, g=0}

さらに、lua_pushnumberで値0をスタックのトップに追加しています。

index
index(逆順)

2
-1
0

1
-2
{r=255, g=0}

次のlua_setfieldで、第二引数-2のテーブルに、スタックのトップの値255を第三引数で指定した要素bの値として追加しています。その際、スタックのトップはポップされます。

index
index(逆順)

1
-2
{r=255, g=0, b=0}

こうして、スタックのトップに完成したテーブルが置かれた状態になりました。

最後に、lua_setglobalを使ってこのテーブルをグローバル変数REDとしてLuaに登録しています。


C++関数の呼び出し

LuaからC++の関数を呼び出す場合、C++側にLuaから呼び出せる形式の関数を作成し、Luaに登録する必要があります。

Luaから呼び出せる関数は、引数が(lua_State *)型であり、戻り値がint型である必要があります。また、インスタンスのメソッドをLuaから直接呼び出すことはできません。

受け取った引数のsin値を計算して返すC++関数は、以下のようになります。

static int l_sin(lua_State *L) {

double d = luaL_checknumber(L, 1);
lua_pushnumber(L, sin(d));
return 1;
}

Luaから渡された引数は、スタックにindex1から順に詰まれています。

lua_checknumberを使用して、引数の値を取得しています。

index
index(逆順)

1
-1
(Luaから渡された引数の値)

次に、sin値を計算した結果をlua_pushnumberを使ってスタックのトップに詰んでいます。

index
index(逆順)

2
-1
(計算したsin値)

1
-2
(Luaから渡された引数の値)

最後に、戻り値の数1を返しています。

これにより、スタックのトップの値がLuaに戻り値として返されます。

この際、引数の値がスタックに残ったままですが、戻り値より下にあるスタックの要素は全て削除されるので問題ありません。

この関数をLuaから呼び出せるようにするには、以下のコードでLuaにC++関数を登録する必要があります。

lua_pushcfunction(L, l_sin);

lua_setglobal(L, "mysin");

スタックのトップにC++関数l_sinを追加し、setglobalでグローバル変数mysinとしてLuaに登録しています。

これを呼び出すLuaのコードは、以下の通りです。

si = mysin(3.14)

Lua側ではLuaの関数と全く同じようにC++の関数を利用できます。


C++関数のモジュール化

上記の方法では、C++関数をグローバル関数としてLuaに登録していましたが、C++関数をモジュールにまとめて登録したい場合もあります。

そのような場合は、まずモジュールにまとめる関数をluaL_Reg型の構造体の配列にまとめます。

static const struct luaL_Reg mylib [] = {

{"mysin", l_sin},
{NULL, NULL}
};

最後の要素は、必ず{NULL, NULL}とします(番兵)。

次に、LuaにC++関数を登録するコードを以下のコードで置き換えます。

luaL_register(L, "mylib", mylib);

これにより、第三引数のluaL_Reg型の構造体の配列に登録されたC++関数が、第二引数の名前mylibのモジュールとしてLuaに登録されます。

mylibモジュールの関数mysinを呼び出すLuaのコードは、以下の通りです。

si = myib.mysin(3.14)

モジュール化することにより、グローバル名前空間を汚すことなくC++関数を追加できます。

また、C++関数をダイナミックライブラリにしてLuaから直接リンクすることもできるそうです。(まだ試していません。)


その他

ここで記載したのは、Programming in Luaの第24章〜第26章の範囲の内容です。

他の重要な要件として、以下のようなものがあると思います。


  • C++の変数とLuaの変数のバインディング

  • C++のインスタンスをLuaで扱う方法

これらについては、第28章の「Cのユーザー定義型」で扱われています。

ユーザー定義型とは、C++のポインタをLuaに渡すことのできる機能のようです。

ただ前述のようにC++のデータにLuaから直接アクセスすることはできないので、C++側にユーザー定義型を受け取ってデータの更新・参照を行う関数を用意し、それをLuaから呼び出す形になるようです。

こちらはまだ詳しく勉強していないので、また後で追記します。