Luaでnewuserdataして、それを利用しようとすると元のデータが破損している【解決】
エラーする状況
デバッグモードでたまに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; }
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を使っていこうかと思いました。