PrevUpHomeNext

concept userdata

Primer can be told to recognize any C++ type as a userdata type. This means that you can construct instances of it within lua, and they can be manipulated by scripts like any lua value.

Primer recognizes userdata types using a trait, primer::traits::userdata. This trait must be specialized for any type which primer will recognize as userdata.

The trait can / should provide three things:

Given this information, primer is able to create the metatable of this userdata type on demand, and store the necessary functions in the permanent objects table so that this userdata can be serialized.

In a pure-lua (no eris) application, not all of these are necessary. The permanent objects table could be skipped. Even the metatable functions could be skipped -- you could recover the metatable yourself using luaL_getmetatable and the key, and adjust it as you like. If there is no userdata name, though, then primer is not able to read this userdata type using primer::read, so there would be little that we could do with it.

Pushing userdata

primer::push_udata is a template function which is used to construct new userdata objects on the stack.

push_udata expects the userdata type as an explicit template parameter, and forwards all arguments to the constructor.

template <typename T, typename... Args>
void push_udata(lua_State * L, Args && ... args);

It is best if your constructor is noexcept, but this is not always possible. If it is not noexcept, then in the event of an exception thrown from the constructor, primer will pop the userdata entry from the stack, and rethrow the exception.

Reading userdata

You can test a stack position for containing userdata of a given type using the primer::test_udata function:

template <typename T>
T * test_udata(lua_State * L, int index);

This function returns a pointer to the underlying T, or, nullptr if the value was not userdata corresponding to that class.

primer::read also contains a built-in overload for references of userdata types:

if (expected<T&> maybe_t = primer::read<T&>(lua_State * L, int index)) { ... }

Usually, you can just use test_udata in code that you write manually -- it's a bit simpler, and the only benefit of the "read" version is that it generates an error message which also describes what type was actually found there.

The read version is important because it means that primer::adapt can handle functions that take references to userdata types as input.

primer::result my_function(lua_State * L, my_userdata & u1, my_userdata & u2);

While primitive types like number, string, and table are generally read from the lua stack by value, as C++ primitives or containers, for userdata types this is usually much less useful. Often, you want to manipulate the userdata value that is actually contained within lua, not some copy of it -- and you don't really want to make copies anyways.

TODO better examples

Adapting userdata methods

PRIMER_ADAPT_USERDATA is an extension of PRIMER_ADAPT which handles member functions of a userdata type.

Functions thus adapted expect to be called using the standard object:method(arg1, arg2) syntax in lua, meaning that the base object is actually the first element on the stack.

This is convenient because sometimes its a little nicer to implement userdata methods as C++ methods rather than free functions. PRIMER_ADAPT can only directly handle free functions.

Userdata trait requirements

A specialization of the userdata trait must provide the following members:

It may provide the following additional members:

If the metatable entry is missing, primer will install a minimalistic metatable for your type.

If the metatable entry is present, but no __gc method is present, primer will generate one which calls the destructor for your type. This is almost always what you want, except for debugging purposes, or if your type is trivial and does not need a destructor call. In that case, you block primer from installing __gc by putting {"__gc", nullptr} in the list -- primer will not register a null pointer as a C function.

If the metatable entry is present, and no __index method is present, primer will implement a common idiom in which the metatable itself is set to be its own index table. Again, you can block this by registering any function or nullptr for __index.

The permanents list is similar to the metatable list, except that those objects will become part of the permanent objects table when persisting and unpersisting a state that has this userdata. See the "API" section for more info.

Alternative syntax

If setting up your metatable is too complex to use the above pattern, for example, if you have entries that need to be set to tables, or you also must add things to the registry to support your type, or something like this, then you can use an alternate method:

In this case you can manually control the population of the metatable. Primer will call luaL_newmetatable using the name first, and your function will be called with the new metatable on top of the stack. Your function is supposed to add members to it and do any other appropriate setup, and return with the stack in the same position that it was found.

Your function will only be called once, when the metatable is initialized, for any given lua State.

More detail

The code which generates the "automatic" metatables from your list of methods is as follows:

using udata = primer::traits::userdata<T>;

static void populate(lua_State * L) {
  using m_t = decltype(udata::metatable);
  const auto & metatable_seq =
    detail::is_L_Reg_sequence<m_t>::adapt(udata::metatable);

  PRIMER_ASSERT_TABLE(L);
  PRIMER_ASSERT_STACK_NEUTRAL(L);

  // Assign the methods to the metatable
  // Use auto in case we use an expanded reg type later.
  bool saw_gc_metamethod = false;
  bool saw_index_metamethod = false;
  bool saw_metatable_metamethod = false;
  constexpr const char * gc_name = "__gc";
  constexpr const char * index_name = "__index";
  constexpr const char * metatable_name = "__metatable";

  // TODO: why can't we just use udata::metatable instead of metatable_seq?
  // it seems that causes an ODR-use or something that isn't otherwise there
  // but I'm not sure why... it might be a compiler bug?
  // This way, while more verbose, seems to allow terser registration of
  // userdata.
  detail::iterate_L_Reg_sequence(
    metatable_seq, [&](const char * name, lua_CFunction func) {
      if (name) {
        if (func) {
          lua_pushcfunction(L, func);
          lua_setfield(L, -2, name);
        }
        if (0 == std::strcmp(name, gc_name)) { saw_gc_metamethod = true; }
        if (0 == std::strcmp(name, index_name)) {
          saw_index_metamethod = true;
        }
        if (0 == std::strcmp(name, metatable_name)) {
          saw_metatable_metamethod = true;
        }
      }
    });

  // If the user did not register __gc then it is potentially (likely) a
  // leak, so install a trivial guy which calls the dtor.
  // Rarely want anything besides this anyways.
  if (!saw_gc_metamethod) {
    lua_pushcfunction(L, &primer::detail::common_gc_impl<T>);
    lua_setfield(L, -2, gc_name);
  }

  // Set the udata_name string also to be the "__metatable" field of the
  // metatable.
  // This means that when the user runs "getmetatable" on an instance,
  // they get a string description instead of the actual metatable, which
  // could be frightening
  if (!saw_metatable_metamethod) {
    lua_pushstring(L, udata::name);
    lua_setfield(L, -2, metatable_name);
  }

  // Set the metatable to be its own __index table, unless user overrides it.
  if (!saw_index_metamethod) {
    lua_pushvalue(L, -1);
    lua_setfield(L, -2, index_name);
  }
}

If you don't provide a metatable field at all, you get this one:

// minimalistic, do-nothing metatable
template <typename T, typename ENABLE = void>
struct metatable {
  using udata = primer::traits::userdata<T>;

  static void populate(lua_State * L) {
    PRIMER_ASSERT_TABLE(L);
    // A minimalistic metatable with __gc
    lua_pushstring(L, udata::name);
    lua_setfield(L, -2, "__metatable");
    lua_pushcfunction(L, &primer::detail::common_gc_impl<T>);
    lua_setfield(L, -2, "__gc");
  }

  static constexpr int value = 0;
};

PrevUpHomeNext