luaを学びなおしたので、そのまとめ(C++との連携)

更新日:2020年12月29日

はじめに

luaのバージョン5.4.2を対象とします。
C++側からINPUTとなる値をluaスクリプトに渡し、luaスクリプトを実行します。
そして、実行結果のOUTPUTを、テーブルや変数の値を取得する・・・ということを目指します。
C++とluaの値の連携は、疑似的なスタックをluaのライブラリが用意してくれるので、それを使い、値のやり取りをします。

データのやり取りを疑似的なスタックを使用するため、そのスタックの内容が重要となります。
そのため、サンプルソースには「stackDump」というメソッドでスタックを表示しています。
これはサンプルソースにでてこないため、以下にソースを記載します。
(C++なのにprintfを使ってる理由は・・・、昔のソースを流用したからです。特に深い意味はありません。)

また、luaは言語としてコルーチンを実装していますが、このコルーチンを使った処理をC++側から呼び出す処理・・・については扱いません。
私がまともに扱える気がしないこと・・・というのが理由です^^;


#include <stdio.h>
#include <iostream>

#include "lua.hpp"

int stackDump(lua_State* L) {

	printf("----------------------------------\n");
	int sp = lua_gettop(L);
	for (int i = 1; i <= sp; i++) {

		printf(":stackDump  sp : %d  ", i);

		switch (lua_type(L, i)) {
			case LUA_TNONE: {
				printf("  → type : LUA_TNONE");
				break;
			}
			case LUA_TNIL: {
				printf("  → type : LUA_TNIL");
				break;
			}
			case LUA_TBOOLEAN: {
				const char* msg = lua_toboolean(L, i) ? "true" : "false";
				printf("  → type : LUA_TBOOLEAN   value : %s", msg);
				break;
			}
			case LUA_TLIGHTUSERDATA: {
				printf("  → type : LUA_TLIGHTUSERDATA");
				break;
			}
			case LUA_TNUMBER: {
				printf("  → type : LUA_TNUMBER   value : %g", lua_tonumber(L, i));
				break;
			}
			case LUA_TSTRING: {
				printf("  → type : LUA_TSTRING   value : %s", lua_tostring(L, i));
				break;
			}
			case LUA_TTABLE: {
				printf("  → type : LUA_TTABLE");
				break;
			}
			case LUA_TFUNCTION: {
				printf("  → type : LUA_TFUNCTION");
				break;
			}
			case LUA_TUSERDATA: {
				printf("  → type : LUA_TUSERDATA");
				break;
			}
			case LUA_TTHREAD: {
				printf("  → type : LUA_TTHREAD");
				break;
			}
			default: {
				printf("  → type : N/A   value : %s", lua_typename(L, i));
				break;
			}
		}
		printf("\n");
	}
	printf("----------------------------------\n");
	return 0;
}

環境構築

luaをソースからビルドする手順はこちらを参照してください。
上記の手順の続きから始めます。
とりあえず、Debug実行の環境構築をします。

(1)開発用のプロジェクトを作成します(ここでは、名前を「luaStudy」プロジェクトとします)。

(2)luaをソースからビルドするプロジェクトをDebugモードとReleaseモードでビルドします。
すると、「luaCommon」プロジェクトのディレクトリに「Debug」と「Release」ディレクトリが作成されているはずです。

(3)「luaCommon」の中の「Debug」ディレクトリから
  luaLib.dll
  luaLib.lib
の2つを、「luaStudy」プロジェクトの中に、以下のようにコピーします。
・「luaStudy」プロジェクトの中のDebugディレクトリの中に「luaLib.dll」をコピー(Debugディレクトリが無い場合は、「luaStudy」プロジェクトをビルドすれば自動的に作成される)。
・「luaStudy」プロジェクトのディレクトリ直下に「luaLib.lib」をコピーします(直下に置いておけば、ライブラリのパスを通さなくてもよくなるので、とりあえずここに置く)。

(4)「luaStudy」プロジェクトへ以下のように、ライブラリを追加します。
「luaStudy」プロジェクトのプロパティから、「構成プロパティ」→「リンカー」→「入力」の中の「追加の依存ファイル」に「luaLib.lib」を追記します。

(5)luaのライブラリを使用するのに、ヘッダが必要なので、それを持ってきます。
どこでもいいので、どこかにディレクトリを作ります。
そのディレクトリへ、luaのソースから「*.h」と「*.hpp」のファイルをコピーします(ようするに「*.c」と「Makefile」以外のファイル全部)。
その後、「luaStudy」のプロジェクトのプロパティから「構成プロパティ」→「VC++ディレクトリ」の中から「全般」の中にある「インクルードディレクトリ」に、先ほど作成したディレクトリのパスを追加します。

上記手順で、「luaStudy」プロジェクトはDebugモードでC++のソースから、luaの実行ができるようになるはずです。
Releaseモードについても、扱うディレクトリ名が「Release」になるだけで、ほぼ同じ手順でOKです。


他のライブラリを導入するときの手順と特に変わった点はありません。
とてもすんなりと導入できます。

グローバル変数の値を取得する

luaをC++で使う場合はヘッダファイルは「lua.hpp」をincludeすればOK。
サンプルソースは「study1」関数を実行してください。
「D:/my/study/luac/luaScript/study1.lua」においたluaスクリプトをC++のソースの中から実行し、実行した後の状態のグローバル変数を取得してみます。
lua_getglobal関数を使うことで、luaのグローバル変数をスタックに持ってくることができます。
lua_getglobal関数を呼ぶ度に、スタックに値が積み上げられていきます。

スタックのindexは1からはじまり、追加する度に2, 3, 4・・・とインクリメントしていきます。
-1という負の値を使うとスタックの一番上、-2だとスタックの一番上から2番目の要素・・・という指定方法となります。
そして、lua_tointeger関数で、スタック上にある値を数値として取得しています(型変換に失敗するとエラーになるため、事前にlua_isnumber関数で型チェックをしている)。

luaのソース(study1.lua)


-- グローバル変数を定義
width = 1000 + 2000
height = width * 10

C++のソース


#include <iostream>
#include "lua.hpp"

extern int stackDump(lua_State* L);

static void doLua(lua_State* L) {
	// luaスクリプトのパス
	const char* filePath = "D:\\my\\study\\luac\\luaScript\\study1.lua";

	// スクリプトを実行する
	if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {
		throw lua_tostring(L, -1);
	}

	std::cout << "■スクリプト実行直後のスタック" << std::endl;
	stackDump(L);
	lua_getglobal(L, "width");
	std::cout << "■lua_getglobal実行後のスタック(1回目)" << std::endl;
	stackDump(L);
	lua_getglobal(L, "height");
	std::cout << "■lua_getglobal実行後のスタック(2回目)" << std::endl;
	stackDump(L);

	if (!lua_isnumber(L, -2)) {
		throw "widthが数値型ではありません";
	}
	if (!lua_isnumber(L, -1)) {
		throw "heightが数値型ではありません";
	}

	// luaの数値型は通常は64bit型(long long型)。「lua_Integer」が定義されているのでそれを使う。
	lua_Integer width = lua_tointeger(L, -2);
	lua_Integer height = lua_tointeger(L, -1);

	std::cout << "width  : " << width << std::endl;
	std::cout << "height : " << height << std::endl;
}

void study1() {

	lua_State* L = luaL_newstate();
	try {
		luaL_openlibs(L);
		doLua(L);
	}
	catch (const char* msg) {
		std::cout << "エラーが発生しました:" << msg << std::endl;
	}
	catch (...) {
		std::cout << "予期せぬエラーが発生しました" << std::endl;
	}

	lua_close(L);
}

実行結果

■スクリプト実行直後のスタック
----------------------------------
----------------------------------
■lua_getglobal実行後のスタック(1回目)
----------------------------------
:stackDump  sp : 1    → type : LUA_TNUMBER   value : 3000
----------------------------------
■lua_getglobal実行後のスタック(2回目)
----------------------------------
:stackDump  sp : 1    → type : LUA_TNUMBER   value : 3000
:stackDump  sp : 2    → type : LUA_TNUMBER   value : 30000
----------------------------------
width  : 3000
height : 30000

グローバル変数の値を取得する(テーブルの中の値を取得)

tableから値を取得しようとした場合、最初に、lua_getglobalでテーブルをスタックに入れた後、次に、テーブルから取得する項目を、スタックにpushする必要があります。
その後、lua_gettableを呼び出すと、値が取得できます。
そして、lua_gettableを呼び出すと、スタックの一番上にいたテーブルの取得項目の値が消え、その代わり、テーブルから取得した値がスタックに積まれているのが実行結果からわかります。

今回はlua_type関数の使い方として、「showValueType」関数も追加しました。
lua_type関数を使うと、スタックに積まれている値の型が判別可能です。
「LUA_TNONE」という型は、indexを受け入れ可能な関数において無効なindexを指定されたときLUA_TNONE型の値を持つとして処理される・・・ということらしい(?)。
よくわかりませんが、「LUA_TNONE」は「LUA_TNIL」と同じらしいので、特に区別は不要かもしれません。
study2関数を実行してください。

luaのソース


-- グローバル変数を定義
sampleTable = {
	-- 配列の値を定義
	"array1",
	"array2",
	"array3",
	
	-- key/valueの値を定義
	val1 = "abcdefghijklmn",
	val2 = 0xffffffff + 0x1,
}

C++のソース


#include <iostream>
#include "lua.hpp"

extern int stackDump(lua_State* L);

// スタックのトップにある値の型を表示する(lua_type関数で型が取得できる)
static void showValueType(lua_State* L) {
	switch (lua_type(L, -1)) {
	case LUA_TNONE:
		std::cout << "type:" << "none" << std::endl;
		break;
	case LUA_TNIL:
		std::cout << "type:" << "nil" << std::endl;
		break;
	case LUA_TNUMBER:
		std::cout << "type:" << "number" << std::endl;
		break;
	case LUA_TBOOLEAN:
		std::cout << "type:" << "boolean" << std::endl;
		break;
	case LUA_TSTRING:
		std::cout << "type:" << "string" << std::endl;
		break;
	case LUA_TTABLE:
		std::cout << "type:" << "table" << std::endl;
		break;
	case LUA_TFUNCTION:
		std::cout << "type:" << "function" << std::endl;
		break;
	case LUA_TUSERDATA:
		std::cout << "type:" << "userdata" << std::endl;
		break;
	case LUA_TTHREAD:
		std::cout << "type:" << "thread" << std::endl;
		break;
	case LUA_TLIGHTUSERDATA:
		std::cout << "type:" << "lightuserdata" << std::endl;
		break;
	default:
		std::cout << "type:" << "N/A" << std::endl;
	}
}

static void doLua(lua_State* L) {
	// luaスクリプトのパス
	const char* filePath = "D:\\my\\study\\luac\\luaScript\\study2.lua";

	// スクリプトを実行する
	if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {
		throw lua_tostring(L, -1);
	}

	// sampleTableを取得する
	lua_getglobal(L, "sampleTable");
	std::cout << "■lua_getglobalを実行" << std::endl;
	stackDump(L);
	if (!lua_istable(L, -1)) {
		throw "sampleTableがtable型ではありません";
	}

	// sampleTableのval1の値を取得する
	lua_pushstring(L, "val1");
	std::cout << "■lua_pushstringを実行" << std::endl;
	stackDump(L);
	
	lua_gettable(L, -2);
	std::cout << "■lua_gettableを実行" << std::endl;
	stackDump(L);
	showValueType(L);

	if (!lua_isstring(L, -1)) {
		throw "文字列型ではありません";
	}
	const char* val1String = lua_tostring(L, -1);
	std::cout << "val1 : " << val1String << std::endl;

	// 値は取得できたので、スタックから削除する(スタックから1個の要素をpopする)
	lua_pop(L, 1);
	std::cout << "■lua_popを実行" << std::endl;
	stackDump(L);


	// sampleTableの添え字[1]の値を取得する
	lua_pushinteger(L, 1);
	std::cout << "■lua_pushintegerを実行" << std::endl;
	stackDump(L);

	lua_gettable(L, -2);
	std::cout << "■lua_gettableを実行" << std::endl;
	stackDump(L);
	showValueType(L);

	if (!lua_isstring(L, -1)) {
		throw "文字列型ではありません";
	}
	const char* array1String = lua_tostring(L, -1);
	std::cout << "sampleTable[1] : " << array1String << std::endl;
}

void study2() {

	lua_State* L = luaL_newstate();
	try {
		luaL_openlibs(L);
		doLua(L);
	}
	catch (const char* msg) {
		std::cout << "エラーが発生しました:" << msg << std::endl;
	}
	catch (...) {
		std::cout << "予期せぬエラーが発生しました" << std::endl;
	}

	lua_close(L);
}

実行結果

■lua_getglobalを実行
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
----------------------------------
■lua_pushstringを実行
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
:stackDump  sp : 2    → type : LUA_TSTRING   value : val1
----------------------------------
■lua_gettableを実行
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
:stackDump  sp : 2    → type : LUA_TSTRING   value : abcdefghijklmn
----------------------------------
type:string
val1 : abcdefghijklmn
■lua_popを実行
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
----------------------------------
■lua_pushintegerを実行
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
:stackDump  sp : 2    → type : LUA_TNUMBER   value : 1
----------------------------------
■lua_gettableを実行
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
:stackDump  sp : 2    → type : LUA_TSTRING   value : array1
----------------------------------
type:string
sampleTable[1] : array1

テーブルの中を走査する

Luaの中のテーブルを全部舐めるには「lua_next」関数を使用します。
lua_next関数を使う場合、lua_nextを呼び出すと、スタックにkeyとvalueの値が積まれます。
そして、値を取得し終わったら、keyとvalueの2個をスタックから削除したくなりますが、1個だけ(valueのみ)を削除します。

そのほか、lua_gettop関数はスタックの一番上の添え字を返します(スタックに要素が5個ある場合、5を返す)。

その他もろもろ、色々面倒なコードになってしまいましたが・・・。
ちなみに、luaには配列という概念はあんまりないです。
全部連想配列です。
なので「"XXXXXXXXXXXX1"」なんかは、キー値が数値の「2」となります(キー名が明示されていない要素の中で2番目にあるので)。
ソースでは簡略化のため、luaのテーブルには、文字列か数値しかないことを前提にしています(それ以外の場合は「N/A」となる)。

study3関数を実行してください。

luaのソース


-- グローバル変数を定義
sampleTable = {
	
	key1 = {
		"key1_aaaa",
		"key1_bbbb"
	},
	key2 = {
		"key2_aaaa",
		"key2_bbbb",
		key2UnderX = {
			"key2UnderX_aaaaa",
			"key2UnderX_bbbbb"
		},
		key2UnderZ = {
			"key2UnderZ_aaaaa",
			"key2UnderZ_bbbbb"
		}
	},
	{
		"AAAAAAA",
		"BBBBBBB",
	},
	"XXXXXXXXXXXX1",
	"XXXXXXXXXXXX2"
}

C++のソース


#include <iostream>
#include <string>
#include "lua.hpp"

extern int stackDump(lua_State* L);


static void textIndent(int count) {
	for (int i = 0; i < count; i++) {
		std::cout << " ";
	}
}
static void showTextKeyOnly(lua_State* L) {
	textIndent(lua_gettop(L));

	int keyPos = -2;
	if (lua_isinteger(L, keyPos))std::cout << lua_tointeger(L, keyPos) << "(table) : \n";
	else if (lua_isstring(L, keyPos))std::cout << lua_tostring(L, keyPos) << "(table) : \n";
	else std::cout << "N/A" << "(table) : \n";
}

static void showTextKeyValue(lua_State* L) {
	textIndent(lua_gettop(L));

	int keyPos = -2;
	int valuePos = -1;
	if (lua_isinteger(L, keyPos))std::cout << lua_tointeger(L, keyPos) << " : ";
	else if (lua_isstring(L, keyPos))std::cout << lua_tostring(L, keyPos) << " : ";
	else std::cout << "N/A" << " : ";

	if (lua_isinteger(L, valuePos))std::cout << lua_tointeger(L, valuePos) << "\n";
	else if (lua_isstring(L, valuePos))std::cout << lua_tostring(L, valuePos) << "\n";
	else std::cout << "N/A" << "\n";
}

static void showTable(lua_State* L, int stackIndex) {

	//stackDump(L);
	int stackHead = lua_gettop(L);
	lua_pushnil(L);
	while (lua_next(L, stackHead) != 0) {
		
		//stackDump(L);
		if (lua_istable(L, -1)) {
			showTextKeyOnly(L);
			showTable(L, lua_gettop(L));
		} else {
			// keyを出力
			showTextKeyValue(L);
		}

		// keyとvalueの2個をpopしてはいけない。keyだけ残す(lua_nextの中で使っているっぽい))
		lua_pop(L, 1);
		//stackDump(L);
	}
}

static void doLua(lua_State* L) {
	// luaスクリプトのパス
	const char* filePath = "D:\\my\\study\\luac\\luaScript\\study3.lua";

	// スクリプトを実行する
	if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {
		throw lua_tostring(L, -1);
	}

	// スタックの最初にsampleTableを取得する
	lua_getglobal(L, "sampleTable");
	std::cout << "■lua_getglobalを実行" << std::endl;
	stackDump(L);
	if (!lua_istable(L, -1)) {
		throw "sampleTableがtable型ではありません";
	}

	std::cout << "■テーブルの中を走査する" << std::endl;
	showTable(L, 1);
}

void study3() {

	lua_State* L = luaL_newstate();
	try {
		luaL_openlibs(L);
		doLua(L);
	}
	catch (const char* msg) {
		std::cout << "エラーが発生しました:" << msg << std::endl;
	}
	catch (...) {
		std::cout << "予期せぬエラーが発生しました" << std::endl;
	}

	lua_close(L);
}

実行結果

■lua_getglobalを実行
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
----------------------------------
■テーブルの中を走査する
   1(table) :
     1 : AAAAAAA
     2 : BBBBBBB
   2 : XXXXXXXXXXXX1
   3 : XXXXXXXXXXXX2
   key1(table) :
     1 : key1_aaaa
     2 : key1_bbbb
   key2(table) :
     1 : key2_aaaa
     2 : key2_bbbb
     key2UnderZ(table) :
       1 : key2UnderZ_aaaaa
       2 : key2UnderZ_bbbbb
     key2UnderX(table) :
       1 : key2UnderX_aaaaa
       2 : key2UnderX_bbbbb

luaの関数を呼び出す(関数にはtableを引数に渡す、戻り値は文字列型)

luaには、テーブルを引数に取る関数(戻り値は文字列)を作成し、それをC++から呼び出してみます。

テーブルのデータを作るにはlua_newtable関数を実行します。
するとスタック上にテーブル型値が作成されます。
その後、テーブルに要素を追加するには、lua_pushintegerなどの関数(lua_pushXXXXX系の関数がデータ型毎に用意されている)を呼び出しスタックにデータを追加後、配列として指定のindexに要素を追加するにはlua_seti関数を使います。
キー名を指定して要素を追加するには、lua_setfield関数を使います。
ようするに、追加したいデータをスタックにpushした後に、lua_seti関数、もしくはlua_setfield関数を呼び出すことで、テーブルに値が追加されます(このあたりの動きは、stackDump関数を逐次呼び出し、動きを見てみると分かりやすい)。
(※テーブルに値をセットした後、lua_setglobalの引数に変数名を付けて呼び出すと、このテーブルはグローバル変数になります。今回はグローバル変数にする必要がないので、呼び出しません。)

luaの関数を呼び出すにはlua_pcall関数を使います(lua_call関数もありますが、こちらは実行した関数の処理の中で異常があった場合は、その時点で処理を終了(プロセスがその時点で終了させられる)してしまいます)。
グローバル定義されている関数(ローカルに定義されている関数の呼び出し方法は、よくわからなかった)を、引数に指定したlua_getglobal関数を実行します(これでスタックに関数が追加される)。
次に、関数の引数を順次pushします(ソースでは、ローカルで定義している(=スタック上にある)テーブルをpushしている)。
引数の順番通りに、スタックにpushします(サンプルでは引数が1個しかないので、1回のみ)。
その後、lua_pcall関数を呼び出せば、関数が実行されます。

関数内でエラー(local a = 10/nil・・・みたいな実行時のエラー)が発生した場合は、戻り値がLUA_OK以外の値が返ります。
(※しれっと最初のほうで「if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {」という処理をしていますが、これも同様にluaスクリプトを実行しています。ただ、実行する命令が書いてないので(関数の定義のみ)、何もしないで処理が返ってきているだけです。 スクリプト全体のパースやれ読み込みはしているので、文法エラーなどがあれば、この時点でエラーになります)

luaのソース


--[[
以下のようなテーブルを引数にとり、合計金額+通貨単位を連結した文字列を返す
valTable = {
	100, 200, 300, 400, 500,
	CurrencyUnit = "$"
}
]]--
function calc(valTable)
	local currencyUnit = ""
	local calcResult = 0
	
	for key, value in pairs(valTable) do
		if type(key) == "number" then
			calcResult = calcResult + value
		else
			if key == "CurrencyUnit" then
				currencyUnit = value
			end
		end
		local a = 0
	end
	
	return currencyUnit..calcResult
end

C++のソース


#include <iostream>
#include <string>
#include "lua.hpp"

extern int stackDump(lua_State* L);

static void doLua(lua_State* L) {
	// luaスクリプトのパス
	const char* filePath = "D:\\my\\study\\luac\\luaScript\\study4.lua";

	// スクリプトを実行する
	if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {
		throw lua_tostring(L, -1);
	}

	/*
	以下のテーブルを作成する(変数名「t」というものは存在しない(スタックの中にデータのみが存在する))
	local t = {
		100, 200, 300,
		CurrencyUnit = "$"
	}
	*/
	// テーブルを作成する
	lua_newtable(L);
	std::cout << "■lua_newtableを実行後" << std::endl;
	stackDump(L);
		
	// t[1]=100
	lua_pushinteger(L, 100);	// スタックに100をpush
	lua_seti(L, -2, 1);			// t[1]=100が実行されスタックからpop
	// t[2]=200
	lua_pushinteger(L, 200);	// スタックに200をpush
	lua_seti(L, -2, 2);			// t[2]=200が実行されスタックからpop
	// t[3]=300
	lua_pushinteger(L, 300);	// スタックに300をpush
	lua_seti(L, -2, 3);			// t[3]=300が実行されスタックからpop
	// t["CurrencyUnit"] = "$"
	lua_pushstring(L, "$");		// スタックに"$"をpush
	lua_setfield(L, -2, "CurrencyUnit");	// t["CurrencyUnit"] = "$"が実行されスタックからpop

	std::cout << "■テーブルを作成し、値をセットし終わった時のスタック" << std::endl;
	stackDump(L);

	// 「calc」関数を呼び出す
	lua_getglobal(L, "calc");
	lua_pushvalue(L, -2);	// スタック上の、指定したindex値をスタックにpushする(なので、LUA_TTABLEが[1]と[3]にある状態になる)

	std::cout << "■関数呼び出し(lua_pcall)実行直前のスタック" << std::endl;
	stackDump(L);


	// 関数を呼び出す(正常に終了したかは、戻り値がLUA_OKであるかをチェックすることで可能)
	// lua_pcallの引数は
	// 第二引数に引数の数が何個あるか
	// 第三引数に戻り値が何個あるか
	// 第四引数はエラーハンドラ関数のスタック上の位置(不要な場合は0を指定)
	int callResult = lua_pcall(L, 1, 1, 0);
	switch (callResult) {
	case LUA_OK:
		std::cout << "関数の呼び出しが正常に終了しました" << std::endl;
		break;
	case LUA_ERRRUN:	// fall through
	case LUA_ERRMEM:	// fall through
	case LUA_ERRERR:	// fall through
	default:
		std::cout << "関数の呼び出しで異常が発生しました:" << callResult << std::endl;
		return;
	}

	std::cout << "■関数呼び出し(lua_pcall)後のスタック" << std::endl;
	stackDump(L);

	// 関数の実行結果(戻り値)が、スタック上にセットされるので、その値を取得
	const char* msg = lua_tostring(L, -1);
	lua_pop(L, 1);	// 戻り値を取得したので、スタックから要素を削除

	// 処理結果(calc関数の戻り値)を表示
	std::cout << "result : " << msg << std::endl;
}

void study4() {

	lua_State* L = luaL_newstate();
	try {
		luaL_openlibs(L);
		doLua(L);
	}
	catch (const char* msg) {
		std::cout << "エラーが発生しました:" << msg << std::endl;
	}
	catch (...) {
		std::cout << "予期せぬエラーが発生しました" << std::endl;
	}

	lua_close(L);
}

実行結果

■lua_newtableを実行後
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
----------------------------------
■テーブルを作成し、値をセットし終わった時のスタック
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
----------------------------------
■関数呼び出し(lua_pcall)実行直前のスタック
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
:stackDump  sp : 2    → type : LUA_TFUNCTION
:stackDump  sp : 3    → type : LUA_TTABLE
----------------------------------
関数の呼び出しが正常に終了しました
■関数呼び出し(lua_pcall)後のスタック
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
:stackDump  sp : 2    → type : LUA_TSTRING   value : $600
----------------------------------
result : $600

上位値

luaからC++の関数を呼び出したとき、その関数内で常に取得可能な値・・・というのを設定することができます。
これを上位値と呼んでいます。

まずは、上位値としてセットしたい値をスタックに積み上げます(今回は2個)。
その後、lua_pushcclosure関数を呼び出し、引数に、対象の関数(study5SampleFunc)と、上位値にしたい値の数(スタックにpushした値の数)を渡します。
注意点としてこの最大値は255です(ということは上位値にセットできる最大個数は255ということ?)。

今回はサンプルとして、グローバル変数に「study5SampleFunc」をセットし、luaから関数を呼べるようにしました。
スクリプトを実行すると、luaからstudy5SampleFuncが呼び出されます。
その時、study5SampleFuncの内部にて上位値を取得しています。
lua_upvalueindexマクロ(上位値の実態としては、スタック上のとても添え字の数が多いところにに保存されています。その上位値が保存されているところのindexを取得するためのマクロです。)に、セットした上位値の何個目の値が欲しいかをセットします。
すると、スタック上の上位値が保存されているindexが計算されて返ってきます。
その値を通常通りlua_tostring関数などセットされているデータに合わせて取得関数の引数に渡してやれば、値を取得できます。

luaのソース


-- 関数を呼び出す
study5SampleFunc()

C++のソース


#include <iostream>
#include <string>
#include "lua.hpp"

extern int stackDump(lua_State* L);

static int study5SampleFunc(lua_State* L) {

	const char* msg1 = lua_tostring(L, lua_upvalueindex(1));
	const char* msg2 = lua_tostring(L, lua_upvalueindex(2));
	const char* msg3 = lua_tostring(L, lua_upvalueindex(3));

	std::cout << "■study5SampleFuncが呼び出されました" << std::endl;
	std::cout << "msg1 : " << msg1 << std::endl;
	std::cout << "msg2 : " << msg2 << std::endl;

	// 未設定の値はnullが返ってくる
	if (msg3 == nullptr) {
		std::cout << "msg3 is null" << std::endl; 
	} else {
		std::cout << "msg3 : " << msg3 << std::endl;
	}
	return 0;
}

static void doLua(lua_State* L) {
	// luaスクリプトのパス
	const char* filePath = "D:\\my\\study\\luac\\luaScript\\study5.lua";

	// クロージャの値(=上位値)を設定(study5SampleFunc関数を実行するときの上位値)
	lua_pushstring(L, "1145144545810");
	lua_pushstring(L, "8/16 07:14:22");
	lua_pushcclosure(L, study5SampleFunc, 2);
	std::cout << "■lua_pushcclosure実行後のスタック" << std::endl;
	stackDump(L);

	// study5SampleFuncを「study5SampleFunc」としてluaのグローバル変数にセットする
	lua_setglobal(L, "study5SampleFunc");
	std::cout << "■lua_setglobal実行後のスタック" << std::endl;
	stackDump(L);

	// スクリプトを実行する
	if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {
		throw lua_tostring(L, -1);
	}
}

void study5() {

	lua_State* L = luaL_newstate();
	try {
		luaL_openlibs(L);
		doLua(L);
	}
	catch (const char* msg) {
		std::cout << "エラーが発生しました:" << msg << std::endl;
	}
	catch (...) {
		std::cout << "予期せぬエラーが発生しました" << std::endl;
	}

	lua_close(L);
}

実行結果

■lua_pushcclosure実行後のスタック
----------------------------------
:stackDump  sp : 1    → type : LUA_TFUNCTION
----------------------------------
■lua_setglobal実行後のスタック
----------------------------------
----------------------------------
■study5SampleFuncが呼び出されました
msg1 : 1145144545810
msg2 : 8/16 07:14:22
msg3 is null

レジストリ

C++からluaに関数を共有するための便利な仕組みとして、レジストリが用意されています。
C++にて、luaL_Reg(luaL_Regは名前と関数ポインタをもつ構造体)の配列を作ります。
そして、それをluaL_setfuncs関数でluaにセットすると、luaスクリプトから名前を使って関数を呼び出せるようになります。

名前には、以下のものは使用しないというルールがあります。
・key値がアンダースコアで始まる文字列キー(luaがこれらを内部的に使うので)
・key値が大文字で始まる文字列キー(luaがこれらを内部的に使うので)
・key値が整数(リファレンスなどの目的があって使う場合はOK。そういった特定用途以外ではNG。)

そして、luaL_setfuncs関数を呼び出す前にスタックに値をセットしておき、luaL_setfuncs関数の第3引数にスタックにセットした値の個数を指定すると、それは第2引数で指定した関数たちが共通でアクセスできる上位値とすることができます(サンプルでは2個の上位値をセットしています)。
不要な場合は、スタックに値のセットも不要ですし、luaL_setfuncs関数の第3引数は0でOKです。

また、関数の戻り値ですが、戻り値がある場合はスタックにpushしてreturnの値に戻り値の個数を返します。
戻り値が無い場合は0をreturnすればOKです。

luaのコード


aiueoLib.func1(12345, "aiueo")
local result1, result2 = aiueoLib.func2()

print("\n(*)lua script output -------")
print("aiueoLib.func2 return value : ", result1, result2)
print("------------------------------")

C++のコード



#include <iostream>
#include <string>
#include "lua.hpp"

extern int stackDump(lua_State* L);

static int regFunc1(lua_State* L) {
	std::cout << "■regFunc1が呼び出されました。" << std::endl;
	stackDump(L);

	// 引数はスタックにセットされている
	int stackCount = lua_gettop(L);
	for (int i = 1; i <= stackCount; i++) {
		if (lua_isstring(L, i)) {
			std::cout << "arg[" << i << "] : " << lua_tostring(L, i) << std::endl;
		} else if (lua_isinteger(L, i)) {
			std::cout << "arg[" << i << "] : " << lua_tointeger(L, i) << std::endl;
		} else {
			std::cout << "arg[" << i << "] : ?" << std::endl;
		}
	}

	// 上位値を取得する
	lua_Integer upvalue1 = lua_tointeger(L, lua_upvalueindex(1));
	const char* upvalue2 = lua_tostring(L, lua_upvalueindex(2));
	std::cout << "upvalue[1] : " << upvalue1 << std::endl;
	std::cout << "upvalue[2] : " << upvalue2 << std::endl;

	// luaスクリプトの関数の戻り値が無い場合は0を返す
	return 0;
}
static int regFunc2(lua_State* L) {
	std::cout << "■regFunc2が呼び出されました。" << std::endl;
	stackDump(L);

	// 上位値を取得する
	lua_Integer upvalue1 = lua_tointeger(L, lua_upvalueindex(1));
	const char* upvalue2 = lua_tostring(L, lua_upvalueindex(2));
	std::cout << "upvalue[1] : " << upvalue1 << std::endl;
	std::cout << "upvalue[2] : " << upvalue2 << std::endl;

	// luaスクリプトの関数の戻り値をセットする
	lua_pushinteger(L, 334);
	lua_pushinteger(L, 114514);
	return 2;	// 戻り値が何個あるかをセット
}

// luaL_Regは、名前と関数ポインタの構造体
static luaL_Reg luaFuncArray[] = {
	{ "func1", regFunc1 },
	{ "func2", regFunc2 },
	{ nullptr, nullptr }	// 配列の最後には、番兵としてnullのデータを置く必要がある
};

static void doLua(lua_State* L) {
	// luaスクリプトのパス
	const char* filePath = "D:\\my\\study\\luac\\luaScript\\study6.lua";

	// テーブルを作成する(lua_newtableは要素数ゼロのテーブルを作るが、こちらは引数で渡した配列の要素数の要素を持った配列を作成する)
	luaL_newlibtable(L, luaFuncArray);

	// luaFuncArrayで指定した関数が共通で取得できる上位値をセットする
	lua_pushinteger(L, 114514);
	lua_pushstring(L, "8/16 07:14:22");

	std::cout << "■luaL_setfuncsを呼び出し前" << std::endl;
	stackDump(L);
	luaL_setfuncs(L, luaFuncArray, 2);	// 第3引数にはセットした上位値の個数を指定。もし上位値が無いならば0を指定する。
	lua_setglobal(L, "aiueoLib");
	std::cout << "■lua_setglobalを呼び出し前" << std::endl;
	stackDump(L);

	// スクリプトを実行する
	if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {
		throw lua_tostring(L, -1);
	}

}

void study6() {

	lua_State* L = luaL_newstate();
	try {
		luaL_openlibs(L);
		doLua(L);
	}
	catch (const char* msg) {
		std::cout << "エラーが発生しました:" << msg << std::endl;
	}
	catch (...) {
		std::cout << "予期せぬエラーが発生しました" << std::endl;
	}

	lua_close(L);
}

実行結果

■luaL_setfuncsを呼び出し前
----------------------------------
:stackDump  sp : 1    → type : LUA_TTABLE
:stackDump  sp : 2    → type : LUA_TNUMBER   value : 114514
:stackDump  sp : 3    → type : LUA_TSTRING   value : 8/16 07:14:22
----------------------------------
■lua_setglobalを呼び出し前
----------------------------------
----------------------------------
■regFunc1が呼び出されました。
----------------------------------
:stackDump  sp : 1    → type : LUA_TNUMBER   value : 12345
:stackDump  sp : 2    → type : LUA_TSTRING   value : aiueo
----------------------------------
arg[1] : 12345
arg[2] : aiueo
upvalue[1] : 114514
upvalue[2] : 8/16 07:14:22
■regFunc2が呼び出されました。
----------------------------------
----------------------------------
upvalue[1] : 114514
upvalue[2] : 8/16 07:14:22

(*)lua script output -------
aiueoLib.func2 return value :   334     114514
------------------------------

ユーザー定義型

ユーザー定義型とは、メモリの管理はluaが行います(メモリの確保、解放はlua側責任を持つ)。
luaからすると、ただのメモリのかたまりで、中身が何なのか関知はしません。
C++側がluaの関数を使い、メモリを確保し、書き込み、そのデータをluaスクリプトを介して、別のC++の関数にデータを受け渡すといったことが可能です。

サンプルでは、func1関数にて、1024バイトのメモリを確保し、それを戻り値としています。
そして、func2関数は引数でユーザー定義型を受け取り、C++の関数内でそのデータにアクセスしています。

luaのソースコード


-- ユーザー定義型の戻り値を取得
local returnVal = aiueoLib.func1()
print("returnVal type : ", type(returnVal))

-- func2にユーザー定義型を引数に渡し、呼び出す
aiueoLib.func2(returnVal)

C++のソースコード


#include <iostream>
#include <string>
#include "lua.hpp"

extern int stackDump(lua_State* L);

static int regFunc1(lua_State* L) {
	std::cout << "■regFunc1が呼び出されました。" << std::endl;

	// ユーザー定義型を作成する(luaが管理するメモリとなる)
	char* val = (char*)lua_newuserdata(L, 1024);
	stackDump(L);

	// 0クリアし、aiueoという文字列をセットしてみる
	memset(val, 0, 1024);
	memcpy(val, "aiueo", 5);

	// ユーザー定義型を関数の戻り値として返す
	return 1;
}

static int regFunc2(lua_State* L) {
	std::cout << "■regFunc2が呼び出されました。" << std::endl;
	stackDump(L);

	// 引数にユーザーデータ型がセットされている想定
	if (!lua_isuserdata(L, -1)) {
		std::cout << "引数がユーザーデータ型ではありません" << std::endl;
		return 0;
	}

	// ユーザーデータ型のポインタを取得
	char* data = (char*)lua_touserdata(L, -1);
	std::cout << "引数で渡ってきたユーザーデータ型:" << data << std::endl;
	
	return 0;
}

static luaL_Reg luaFuncArray[] = {
	{ "func1", regFunc1 },
	{ "func2", regFunc2 },
	{ nullptr, nullptr }	// 配列の最後には、番兵としてnullのデータを置く必要がある
};


static void doLua(lua_State* L) {
	// luaスクリプトのパス
	const char* filePath = "D:\\my\\study\\luac\\luaScript\\study7.lua";

	// レジストリに関数を設定する
	luaL_newlibtable(L, luaFuncArray);
	luaL_setfuncs(L, luaFuncArray, 0);
	lua_setglobal(L, "aiueoLib");

	// スクリプトを実行する
	if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {
		throw lua_tostring(L, -1);
	}
}

void study7() {

	lua_State* L = luaL_newstate();
	try {
		luaL_openlibs(L);
		doLua(L);
	}
	catch (const char* msg) {
		std::cout << "エラーが発生しました:" << msg << std::endl;
	}
	catch (...) {
		std::cout << "予期せぬエラーが発生しました" << std::endl;
	}

	lua_close(L);
}

実行結果

■regFunc1が呼び出されました。
----------------------------------
:stackDump  sp : 1    → type : LUA_TUSERDATA
----------------------------------
returnVal type :        userdata
■regFunc2が呼び出されました。
----------------------------------
:stackDump  sp : 1    → type : LUA_TUSERDATA
----------------------------------
引数で渡ってきたユーザーデータ型:aiueo

ライトユーザーデータ

ライトユーザーデータは、ユーザー定義型とほぼ同じですが、違う点として、メモリの管理はC++側が責任を持ちます。
なので、メモリの確保もC++のnewをつかって実施し、メモリの解放もC++側で実行します。

このサンプルは、さきほどのユーザー定義型のサンプルソースを一部変更し、ライトユーザーデータにしたものです。

luaのソースコード


-- ユーザー定義型の戻り値を取得
local returnVal = aiueoLib.func1()
print("returnVal type : ", type(returnVal))

-- func2にユーザー定義型を引数に渡し、呼び出す
aiueoLib.func2(returnVal)

C++のソースコード


#include <iostream>
#include <string>
#include "lua.hpp"

extern int stackDump(lua_State* L);

static int regFunc1(lua_State* L) {
	std::cout << "■regFunc1が呼び出されました。" << std::endl;

	// メモリを確保する
	char* val = new char[1024];
	
	// 0クリアし、aiueoという文字列をセットしてみる
	memset(val, 0, 1024);
	memcpy(val, "aiueo", 5);

	// ライトユーザーデータを関数の戻り値として返す
	lua_pushlightuserdata(L, val);
	stackDump(L);
	return 1;
}

static int regFunc2(lua_State* L) {
	std::cout << "■regFunc2が呼び出されました。" << std::endl;
	stackDump(L);

	// 引数にユーザーデータ型がセットされている想定
	if (!lua_islightuserdata(L, -1)) {
		std::cout << "引数がライトユーザーデータではありません" << std::endl;
		return 0;
	}

	// ユーザーデータ型のポインタを取得
	char* data = (char*)lua_touserdata(L, -1);	// ユーザーデータ型であってもライトユーザーデータであってもlua_touserdataを使う
	std::cout << "引数で渡ってきたユーザーデータ:" << data << std::endl;

	// メモリを解放する
	delete[] data;

	return 0;
}

static luaL_Reg luaFuncArray[] = {
	{ "func1", regFunc1 },
	{ "func2", regFunc2 },
	{ nullptr, nullptr }	// 配列の最後には、番兵としてnullのデータを置く必要がある
};


static void doLua(lua_State* L) {
	// luaスクリプトのパス
	const char* filePath = "D:\\my\\study\\luac\\luaScript\\study8.lua";

	// レジストリに関数を設定する
	luaL_newlibtable(L, luaFuncArray);
	luaL_setfuncs(L, luaFuncArray, 0);
	lua_setglobal(L, "aiueoLib");

	// スクリプトを実行する
	if (luaL_loadfile(L, filePath) || lua_pcall(L, 0, 0, 0)) {
		throw lua_tostring(L, -1);
	}
}

void study8() {

	lua_State* L = luaL_newstate();
	try {
		luaL_openlibs(L);
		doLua(L);
	}
	catch (const char* msg) {
		std::cout << "エラーが発生しました:" << msg << std::endl;
	}
	catch (...) {
		std::cout << "予期せぬエラーが発生しました" << std::endl;
	}

	lua_close(L);
}

実行結果

■regFunc1が呼び出されました。
----------------------------------
:stackDump  sp : 1    → type : LUA_TLIGHTUSERDATA
----------------------------------
returnVal type :        userdata
■regFunc2が呼び出されました。
----------------------------------
:stackDump  sp : 1    → type : LUA_TLIGHTUSERDATA
----------------------------------
引数で渡ってきたユーザーデータ:aiueo

ページの一番上へ