In the following example a userdata
value is created of type MyType
and a table is created with a metafunction __tostring
which calls LI_MyType__tostring
. The code creates a closure-based lua OOP. My gripe with the example provided is it appears as though there is only one way to associate userdata
with a method call, via upvalues. In and of itself, this isn't problematic unless I want to share the same metatable across instances.
In an ideal world - and what I'm hoping to unearth with this question - is there a way to associate an upvalue with a value (e.g. userdata
) without associating it with a function call via an upvalue? I'm hoping there is a trick that will let me continue to use closure-based lua OOP and share the same metatable across instances. I'm not optimistic, but I figured I'd ask to see if someone has a suggestion or a non-obvious trick.
using FuncArray = std::vector<const ::luaL_Reg>;
static const FuncArray funcs = {
{ "__tostring", LI_MyType__tostring },
};
int LC_MyType_newInstance(lua_State* L) {
auto userdata = static_cast<MyType*>(lua_newuserdata(L, sizeof(MyType)));
new(userdata) MyType();
// Create the metatable
lua_createtable(L, 0, funcs.size()); // |userdata|table|
lua_pushvalue(L, -2); // |userdata|table|userdata|
luaL_setfuncs(L, funcs.data(), 1); // |userdata|table|
lua_setmetatable(L, -2); // |userdata|
return 1;
}
int LI_MyType__tostring(lua_State* L) {
// NOTE: Blindly assume that upvalue 1 is my userdata
const auto n = lua_upvalueindex(1);
lua_pushvalue(L, n); // |userdata|
auto myTypeInst = static_cast<MyType*>(lua_touserdata(L, -1));
lua_pushstring(L, myTypeInst->str()); // |userdata|string|
return 1; // |userdata|string|
}
I'm hoping there's a way of performing something like (this is pseudo-code!):
// Assume that arg 1 is userdata
int LI_MyType__tostring(lua_State* L) {
const int stackPosition = -1;
const int upvalueIndex = 1;
const auto n = lua_get_USERDATA_upvalue(L, stackPosition, upvalueIndex);
lua_pushvalue(L, n); // |userdata|
auto myTypeInst = static_cast<MyType*>(lua_touserdata(L, -1));
lua_pushstring(L, myTypeInst->str()); // |userdata|string|
return 1; // |userdata|string|
}
I know this is similar to how things would be for the "normal" metatable style of OOP, but I want to keep things closure based and avoid introducing the colon syntax.
Another way of asking this question would be, is there a way to share metatables across userdata
instances while using a closure-based OOP? Using lua's syntax from the scripting side of things, I don't think it's possible, but I'm hoping there's something that can be done on the C side of things.
UPDATE (2013-10-10): Based on @lhf's answer to use lua_setuservalue()
and lua_getuservalue()
the protocol I've settled on which allows me to reuse metatables is this:
- Register a single metatable object using
luaL_newmetatable()
. This metatable can now be shared acrossuserdata
instances because no upvalues are used when registering the metatable. - Create a
userdata
value (lua_newuserdata()
). - Assign the correct metatable to the
userdata
value (lua_setmetatable()
). - Create and populate an instance method calls/attributes table with one upvalue, the
userdata
. - Use
lua_setuservalue()
onuserdata
to store a reference to the per-instance attribute/method table. - Change various metamethods (e.g.
__index
) to use theuserdata
's uservalue table.
As a consequence:
- upvalues are never used in metamethods
- upvalues are only used in a value's instance methods
- there is only one extra table per instance of a given class
It's still not possible to escape creating a method/attribute table per userdata, but that overhead is nominal. It would be nice if obj.myMethod()
would pass obj
to function myMethod()
somehow without using :
, but that's exactly what :
does because this isn't possible another way (unless you do make use of an upvalue).