In lua is there a way to bind an upvalue to a userdata value instead of a function?

StackOverflow https://stackoverflow.com/questions/19262746

  •  30-06-2022
  •  | 
  •  

Question

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:

  1. Register a single metatable object using luaL_newmetatable(). This metatable can now be shared across userdata instances because no upvalues are used when registering the metatable.
  2. Create a userdata value (lua_newuserdata()).
  3. Assign the correct metatable to the userdata value (lua_setmetatable()).
  4. Create and populate an instance method calls/attributes table with one upvalue, the userdata.
  5. Use lua_setuservalue() on userdata to store a reference to the per-instance attribute/method table.
  6. Change various metamethods (e.g. __index) to use the userdata'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).

Était-ce utile?

La solution

lua_setuservalue seems to be exactly what you need. There is also of course lua_getuservalue.

(I'm skipping the C++ code and answering the question in the title.)

Autres conseils

I don't think you should be trying to do it exactly, for a few reasons.

  1. If you call object.method(), and you try to infer the object from the instance that was created, you're blocking your ability to pass function pointers around that behave on any object given.
  2. You have cyclic references to objects that will never get garbage collected (each instance's function pointing back to the instance).

Just get the object from slot 1, and check its type matches your userdata. (luaL_checkudata)

If its not an object and tostring is called for example, just output that its a class of object name, instead of the instance details. It makes far more sense, and may well make debugging simpler if the object reports what it actually is, rather than trying to be too clever and misleading you.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top