読者です 読者をやめる 読者になる 読者になる

きままにブログ

プログラミングを主とした私のメモ帳です。寂しいのでコメントください笑

Luaでnewuserdataして、それを利用しようとすると元のデータが破損している【解決】

C++ Lua

エラーする状況

デバッグモードでたまにRTTI情報を持っていないと怒られる。

#include <iostream>
#include <Lua53/lua.hpp>

using std::cout;
using std::endl;

struct Object {
	virtual ~Object() {
		cout << "Object::~Object();" << endl;
	}
};

struct C : public Object {
	int x;
	C(int x) : x(x) {
		//cout << "C::C();" << endl;
	}
	~C() {
		cout << "C::~C();" << endl;
	}
};

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	C* c = static_cast<C*>(lua_newuserdata(L, sizeof(C)));
	new(c) C(x);
	*static_cast<Object**>(lua_newuserdata(L, sizeof(Object*))) = c;

	return 1;
}

int L_use(lua_State* L) {
	Object* object = *static_cast<Object**>(lua_touserdata(L, 1));
	C* c = dynamic_cast<C*>(object); // ここでデバッガの実行時エラー!

	//cout << "c.x = " << c->x << endl;

	return 0;
}

int main() {
	lua_State* L = luaL_newstate();

	luaL_Reg funcs[] = {
		{ "construct", L_construct },
		{ "use", L_use },
		{ nullptr, nullptr }
	};
	
	luaL_newlibtable(L, funcs);
	luaL_setfuncs(L, funcs, 0);
	lua_setglobal(L, "MyLib");

	luaL_openlibs(L);
	luaL_loadfile(L, "lua.lua");

	if(lua_pcall(L, 0, 0, 0) != LUA_OK) {
		cout << lua_tostring(L, -1) << endl;
	}

	lua_close(L);

	std::cin.get();
	return 0;
}

Luaスクリプトは次の通り :

print("test");

local c;

for i = 1, 10000 do
	print(i);
	c = MyLib.construct(i);
	MyLib.use(c);
end

10000回ループでなくても、150回程度でも途中で止まることがある。だいたい125~128あたりで止まることが多い。Cのメンバにchar data[128];を入れると、52~53回程度で止まることが確認された。

怒ったり怒らなかったり再現性が今一歩なので調査中…

追記1

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	auto c = static_cast<C*>(lua_newuserdata(L, sizeof(C)));
	new(c)C(x);
	lua_pushlightuserdata(L, static_cast<Object*>(c));

	return 1;
}

int L_use(lua_State* L) {
	Object* object = static_cast<Object*>(lua_touserdata(L, 1));
	C* c = dynamic_cast<C*>(object);

	//cout << "c.x = " << c->x << endl;

	return 0;
}

でもだめみたいです。

追記2

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	auto c = new C(x);
	lua_pushlightuserdata(L, static_cast<Object*>(c));

	return 1;
}

では大丈夫なことから、Luaのメモリ管理の部分に問題がありそうなのは確かです。もしかしたらガベージコレクションが実行されてしまったのかもしれません。

そこで、追記1のコードの元、Luaのプログラムを次のように変えてみました。

print("test");

c = {};

for i = 1, 10 do
	c[i] = MyLib.construct(i);
end

for i = 1, 10 do
	print(i);
	MyLib.use(c[i]);
end

これは実行されます。そこで、10→1000へと変更してみました。すると、useメソッドが3~5回呼び出されただけで先ほどのエラーが出るではありませんか。やはりconstructメソッド内のnewuserdataに問題があるに違いありません。

そもそも、データ自体が破損しています。1000だと次のような出力を得ました。

test
1
258
2
259
3
260
4
261
5
262
6
263
7
264
8
265
9
266
〔以下略〕

追記3

もちろん、

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	auto c = lua_newuserdata(L, sizeof(C));
	new(c)C(x);

	return 1;
}

int L_use(lua_State* L) {
	C* c = static_cast<C*>(lua_touserdata(L, 1));
	
	cout << "c.x = " << c->x << endl;

	return 0;
}

公だったら何も問題はないんですけど、Object*にキャストしないと、いろいろなユーザーデータを受け取るときに型チェックできないんですね。型のチェックができないと動的な処理をする際に困ります。

かといって、無理やりObject*にキャストしても成功する場合がありますがそれはC*とObject*が同じポインタである場合のみです。たとえば多重継承すると、次のように失敗する可能性があります。

struct A {
	virtual ~A() {
		cout << "A::~A();" << endl;
	}
};

struct Object {
	virtual ~Object() {
		cout << "Object::~Object();" << endl;
	}
};

struct C : public A, public Object {
	int x;
	C(int x) : x(x) {
		//cout << "C::C();" << endl;
	}
	virtual ~C() {
		cout << "C::~C();" << endl;
	}
};

struct CD : public A, public Object {
	int y;
	CD(int y) : y(y) {
		//cout << "CD::CD();" << endl;
	}
	virtual ~CD() {
		cout << "CD::~CD();" << endl;
	}
};

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	auto cd = lua_newuserdata(L, sizeof(CD));
	new(cd)CD(x);

	return 1;
}

int L_use(lua_State* L) {
	CD* cd = dynamic_cast<CD*>(static_cast<Object*>(lua_touserdata(L, 1)));
	// うまくいきそうだが、cdがnullptrの可能性がある。
	cout << "c.x = " << cd->y << endl;

	return 0;
}

追記4

試しに__gcに関数を追加してガベージコレクションの動きを見てみたら、useメソッドが実行される前にガベージコレクションが実行されていました。

int L_destruct(lua_State* L) {
	cout << "ですとらくと" << endl; // これが実行されていた
	return 0;
}

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);
	
	auto c = static_cast<C*>(lua_newuserdata(L, sizeof(C)));
	new(c)C(x);
	auto p = static_cast<Object**>(lua_newuserdata(L, sizeof(Object*)));
	*p = c;
	lua_newtable(L);
	lua_pushcfunction(L, L_destruct);
	lua_setfield(L, -2, "__gc"); // -2 : newtable
	lua_setmetatable(L, -2); // -2 : new_userdata, -1 : newtable
	return 1;
}

int L_use(lua_State* L) {
	Object* object = *static_cast<Object**>(lua_touserdata(L, 1));
	C* c = dynamic_cast<C*>(object); // この前に
	
	cout << "c.x = " << c->x << endl;

	return 0;
}

追記5 〔解決策1〕

Luaは、戻り値をスタックに積んだ最後のn個のうち、最初に積んだものから値を返します。すなわち、return 1;なら最後のnewuserdataであるObject*のデータを返すのです。ということは、newuserdataしてももう片方は使われないのだから、回収されて当然といえます。逆に言えば、一緒の寿命となってくれればいいわけです。

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	auto c = static_cast<C*>(lua_newuserdata(L, sizeof(C)));
	new(c)C(x);
	lua_newtable(L);
	lua_pushcfunction(L, L_destruct2); // このファイナライザだけ呼ばれる
	lua_setfield(L, -2, "__gc");
	lua_setmetatable(L, -2);

	auto p = static_cast<Object**>(lua_newuserdata(L, sizeof(Object*)));
	*p = c;
	lua_newtable(L);
	lua_pushcfunction(L, L_destruct); // こっちは呼ばれない
	lua_setfield(L, -2, "__gc");
	lua_setmetatable(L, -2);

	return 1;
}

仕方がないので、アドレスを保持しつつデータも一緒に持つ邪悪なプログラムを書いてみました。

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	void* data = lua_newuserdata(L, sizeof(Object*) + sizeof(C));
	Object** p_object = new(data) Object*;
	data = static_cast<Object*>(data)+1;
	C* p_c = new(data)C(x);
	*p_object = static_cast<Object*>(p_c);

	lua_newtable(L);
	lua_pushcfunction(L, L_destruct);
	lua_setfield(L, -2, "__gc");
	lua_setmetatable(L, -2);

	return 1;
}

int L_use(lua_State* L) {
	void* data = lua_touserdata(L, 1);
	Object* object = static_cast<Object**>(data)[0];
	C* c = dynamic_cast<C*>(object);
	
	cout << "c.x = " << c->x << endl;

	return 0;
}

追記6〔解決策2〕

正直、newしてdeleteしてあげればいいんじゃないか、って思えた。

int L_destruct(lua_State* L) {
	Object* object = *static_cast<Object**>(lua_touserdata(L, 1));
	delete object; // objectはポリモーフィックなものなので仮想デストラクタが呼び出される
	return 0;
}

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	auto p = static_cast<Object**>(lua_newuserdata(L, sizeof(Object*)));
	*p = new C(x); // 一応new, 最適化したければ独自アロケータ使用

	lua_newtable(L);
	lua_pushcfunction(L, L_destruct);
	lua_setfield(L, -2, "__gc");
	lua_setmetatable(L, -2);

	return 1;
}

int L_use(lua_State* L) {
	Object* object = *static_cast<Object**>(lua_touserdata(L, 1));
	C* c = dynamic_cast<C*>(object); // 普通にdynamicすればよし
	
	cout << "c.x = " << c->x << endl;

	return 0;
}

多分下手なアロケータ作るよりnewのほうが効率がいいと思うけど、もしも小さなオブジェクトを多数作ることになるなら、独自のアロケータを作ってもいいと思う。その際は可変のものでなくてはならないけど。

追記7〔比較〕

せっかくなので、new/deleteとガベージコレクションを使った場合の比較を行います。

条件

  • 条件X : int x; のみの軽いクラス
  • 条件Y : int x; char data[1024];の重いクラス
  • 条件a : すぐに破棄されるデータ
n = 50000;

for i = 1, n do
	local c = MyLib.construct(i);
	MyLib.use(c);
end
  • 条件b : しばらく破棄されないデータ
n = 50000;
local c = {};

for i = 1, n do
	c[i] = MyLib.construct(i);
end

for i = 1, n do
	MyLib.use(c[i]);
end

new/delete

int L_destruct(lua_State* L) {
	Object* object = *static_cast<Object**>(lua_touserdata(L, 1));
	delete object;
	return 0;
}

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	auto p = static_cast<Object**>(lua_newuserdata(L, sizeof(Object*)));
	*p = new C(x);

	lua_newtable(L);
	lua_pushcfunction(L, L_destruct);
	lua_setfield(L, -2, "__gc");
	lua_setmetatable(L, -2);

	return 1;
}

int L_use(lua_State* L) {
	Object* object = *static_cast<Object**>(lua_touserdata(L, 1));
	C* c = dynamic_cast<C*>(object);
	
	return 0;
}

Luaのガベコレ

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

int L_construct(lua_State* L) {
	int x = lua_tointeger(L, 1);

	void* data = lua_newuserdata(L, sizeof(Object*)+sizeof(C));
	Object** p_object = new(data)Object*;
	data = static_cast<Object*>(data)+1;
	C* p_c = new(data)C(x);
	*p_object = static_cast<Object*>(p_c);

	lua_newtable(L);
	lua_pushcfunction(L, L_destruct);
	lua_setfield(L, -2, "__gc");
	lua_setmetatable(L, -2);

	return 1;
}

int L_use(lua_State* L) {
	void* data = lua_touserdata(L, 1);
	Object* object = static_cast<Object**>(data)[0];
	C* c = dynamic_cast<C*>(object);

	return 0;
}

結果

new/delete
Xa : 0.89538, 0.829864, 0.840311
Xb : 10.5084, 10.4894, 10.5108
Ya : 0.879873, 0.872933, 0.87552
Yb : 10.8091, 10.4588, 10.472

ガベコレ
Xa : 0.719995, 0.707583, 0.727165
Xb : 10.8347, 10.6688, 10.682
Ya : 3.49195, 3.22564, 3.26311
Yb : 71.5608

つまり、ガベージコレクションは、その対象のデータが大きくなるほど遅くなります。また、データの寿命が短いほど動作が早く、データを保持するとかなり遅くなります。

一方で、new/deleteの場合は、データの量にはよりません。ただし、データの寿命が長いほど動作が遅くなります。というのも、そのために余分にメモリが必要なため確保・開放するのに時間がかかるからです。

軽量なデータを多く扱うことが考えられるので、ガベージコレクションに任せたほうがよさそうですが、総合的に見てそこまで大差が見られなかったので、new/deleteを使っていこうかと思いました。