Reputation: 169
I'm adding Lua Scripting to my game engine (C++) using the Lua C API.
I'm pushing my objects using lua_pushlightuserdata(lua_State *L, void *p)
and when I want to get the objects I use lua_touserdata(lua_State *L, int idx)
, but I don't know what object I'm getting using lua_touserdata()
.
How can I check that?
Here's some example code:
bool LuaScript::InitScript(const char* code, GameObject * container)
{
CloseLua();
bool ret = false;
luaState = LuaNewState();
luaL_openlibs(luaState);
RegisterAPI(luaState);
/*Load the code inside .lua script and set "this" to reference the GameObject
containing the script and "renderer" referencing SpriteRenderer class (GameObject component)
to show you the example.*/
if (luaL_loadstring(luaState, code) == 0) {
lua_pushlightuserdata(luaState, container);
lua_setglobal(luaState, "this");
SpriteRenderer* spr = new SpriteRenderer();
lua_pushlightuserdata(luaState, spr);
lua_setglobal(luaState, "renderer");
ret = LuaUtils::CallFunction(luaState, NULL);
}
else {
LOG_WARNING("Cannot load lua script of '%s': %s", container->name.c_str(), lua_tostring(luaState, -1));
}
return ret;
}
void LuaScript::RegisterAPI(lua_State* luaState)
{
luaL_Reg GameObjectAPI[] =
{
{ "SetActive", SetGameObjectActive },
{ NULL, NULL }
};
LuaUtils::RegisterLibrary(luaState, GameObjectAPI, "gameObject");
}
int LuaScript::SetGameObjectActive(lua_State * luaState)
{
int arguments = lua_gettop(luaState);
if (arguments != 2) {
LOG_WARNING("SetActive(GameObject, bool) takes 2 arguments!");
}
else {
if (lua_islightuserdata(luaState, 1)) {
/*---> Here it's the problem. I'm assuming that this data is a GameObject
but it can be other kind of data like SpriteRenderer.*/
GameObject* go = (GameObject*)lua_touserdata(luaState, 1);
bool active = lua_toboolean(luaState, 2);
go->SetActive(active);
}
}
return 0;
}
.lua Script:
function Start()
gameObject.SetActive(this,false)
gameObject.SetActive(renderer,false)
end
In the above example, gameObject.SetActive(this,false)
works because this
is a GameObject, but gameObject.SetActive(renderer,false)
should not work because renderer
is not a GameObject. But it works because I don't know a way to check if it's a GameObject or SpriteRenderer and the variable go
at line GameObject* go = (GameObject*)lua_touserdata(luaState, 1);
inside LuaScript::SetGameObjectActive(lua_State * luaState)
is malformed (nullptr's, memory errors, etc.) because I'm assigning the data as a GameObject.
Upvotes: 3
Views: 2580
Reputation: 4264
For light userdata, Lua stores only a pointer. The information you seek does not exist, you will have to add it in some way.
Let's have a look at a few ways of doing this:
Full userdata is allocated by Lua and gets all the fancy Lua features (metatables, garbage collection, associated "user value", …). This means that your value isn't just a raw pointer but a full structure with many slots where you can store the type information.
By far the most common way is to use metatables. (This also permits adding methods or overriding operators on your userdata, but the metatable can also be completely empty.) A nice way of using metatables is via luaL_setmetatable
and luaL_checkudata
. (You create a metatable via luaL_newmetatable
, which is intended to be used like
if (luaL_newmetatable( L, "Foo" )) { // where 'Foo' is your type name
// initialize metatable contents here (if any)
} // else metatable already exists
and then you can luaL_setmetatable( L, "Foo" );
to set the metatable of the thing on top of the stack, or Foo *foo = luaL_checkudata( L, i, "Foo" );
to check that the i
th argument of your function is a Foo
and get it (or else throw an error).
(This "user value" is mostly intended to allow having a single shared metatable for all values of one type yet still permit varying per-value information, so it would usually hold another table… as such, this approach is uncommon and just included for completeness.)
Using lua_setuservalue
/ lua_getuservalue
, you can simply store arbitrary Lua values in the 'user value' slot of your userdata. If you have an enum of all types that you use, you can simply store an integer in that field.
Sample scheme (only minimally tested):
void settag( lua_State *L, lua_Integer tag ) {
lua_pushinteger( L, tag );
lua_setuservalue( L, -2 );
}
void *testtag( lua_State *L, int ud, lua_Integer tag ) {
void *p = lua_touserdata( L, ud );
if (p != NULL) { /* really is userdata */
if (lua_getuservalue( L, ud ) == LUA_TNUMBER) { /* uservalue is number */
if (lua_tointeger( L, -1 ) == tag) { /* and tag matches */
return p;
}
}
}
return NULL;
}
void *checktag( lua_State *L, int ud, lua_Integer tag ) {
void *p = testtag( L, ud, tag );
if (p == NULL) luaL_argerror( L, ud, "wrong userdata" );
return p;
}
(For both of these full userdata approaches, remember that Lua does garbage collection and will free your object if it is no longer on the Lua stack or stored in a table somewhere in the Lua state!)
Ensure all values that you push to Lua share a common header. In C, it's easy to make all struct
s start with one (or more) common field(s), C++ classes might be different. But you can just define a wrapper type like (C sample again…)
typedef struct {
int tag;
void *ptr;
} Tagged;
and only push Tagged
values (which can be wrapped immediately before pushing them… or always by default, potentially even by having the type tag inline in your value and avoiding the extra pointer indirection). When getting a value back, first check the tag
before using the ptr
(or the rest of the value, if inlined).
Make sure that everywhere you're dealing with lightuserdata in Lua, you wrap it in tables. In this way, you again have slots to store the extra type info.
A simple scheme would be to have { ptr =
<userdata pointer>, type=
<type name or numeric id or …>}
. Every function that would have taken a bare userdata so far would now expect a table with (at least) these two fields and can then check v.type
before extracting and using v.ptr
.
Either on the Lua or on the C++ side, you can keep a map from void*
/ light userdata to type (ID).
On the C++ side, there's probably some ready-made data structure that you can use for this. (I'm not a C++ programmer, so I have no idea – std::map
might work? How easy/hard that is to get right, I don't know.)
On the Lua side, this is as simple as creating & storing a table (e.g. in the state's registry), then adding type information by lut[ptr] = tag
or checking by lut[ptr] == tag
(or via the equivalent sequence of Lua API functions if done on the C(++) side).
If you set a metatable on the LUT to make the keys weak (__mode = "k"
), fields no longer referenced elsewhere in the Lua state will automatically be freed by the garbage collector. Otherwise, you'll have to manually manage it in some way – the easiest way is probably to extend your free
to also set lut[ptr] = nil
(this can be done unconditionally, doesn't matter if the value was ever passed to Lua).
Variation 1.a is probably easiest. (If you have trouble keeping things anchored in the Lua state, try fixing that because it will make things a lot easier.) If you absolutely cannot fix your anchoring, variation 2.a or 2.b might make sense. Variations 1.b and 3 are mostly academic… they might save you in very special circumstances but are generally not the best idea.
For games specifically, use 1.a (or if you design it in from the start and are sure that you'll not need metatables, go with 2.a. Metatables are one of the core features of Lua, and by not using them you'll be missing out on a lot of stuff. But using Lua without metatables can still be a valid choice, if you know what you're doing.)
Upvotes: 4
Reputation: 13073
It is better to use full user data, as that is bound with more information. However, it is slightly easier to use light-user-data, as it avoids some code.
If the type you put into lightuserdata is derived from the same base class, and you have run-time-type-information you can use the C++ language to query the real type of the object.
Otherwise using a full userdata is the best way to go.
void * mem = lua_newuserdata( L, sizeof( Type) );
returns you a new piece of memory
new (mem) Type( params );
creates the correct type
lua_setmetatable
allows lua to understand the type, and can be queried with lua_getmetatable
in C/C++
The definitive books on this is Programming in lua which is recommended.
The simplest version of your code is as follows, this is not the most correct method...
// somewhere when setting up
luaL_newmetadata( L, "some string" );
Then when creating the lua objects....
SpriteRenderer* spr = new SpriteRenderer();
SpriteRenderer ** data = (SpriteRenderer**)lua_newuserdata( L, sizeof( SpriteRenderer *) ); // hold a pointer.
*data = spr; // now spr is in user data.
luaL_getmetadata( L, "some string" );
lua_setmetadata( L, -2 ); // set meta-data for this object.
Now you can test the type is the correct type....
luaL_checkudata( L, idx, "some string" ); // only works if the type is correct.
Upvotes: 3