PrevUpHomeNext

Userdata

userdata is a special kind of lua value. It cannot be created directly by lua code -- it can only be created by using C API functions. A userdata is essentially a block of memory, owned by lua, which external code has initialized in some way. Scripts are able to pass this "value" around, storing it in variables or passing it to functions. Here's a minimal example of userdata -- an opaque "token" type for use with some API.

struct token {
  int id;
};

int
new_token(lua_State * L) {
  static int count = 0;
  void * udata = lua_newuserdata(L, sizeof(token));
  new (udata) token{count++};
  return 1;
}

int
inspect_token(lua_State * L) {
  if (void * udata = lua_touserdata(L, 1)) {
    std::cout << "Token id: " << static_cast<token *>(udata)->id << std::endl;
  } else {
    std::cout << "Not a userdata" << std::endl;
  }
  return 0;
}

  // ...

  lua_pushcfunction(L, new_token);
  lua_setglobal(L, "new_token");
  lua_pushcfunction(L, inspect_token);
  lua_setglobal(L, "inspect_token");

  const char * script =
    ""
    "x = new_token() "
    "y = new_token() "
    "inspect_token(x) "
    "inspect_token(y) "
    "y = x "
    "inspect_token(y) "
    "inspect_token(5) ";

    assert(LUA_OK == luaL_loadstring(L, script));
    assert(LUA_OK == lua_pcall(L, 0, 0, 0));

This example prints

Token id: 0
Token id: 1
Token id: 0
Not a userdata

While the tokens can be stored in variables, lua can't inspect or change them directly. x cannot be used with lua functions like + that take a number, and x.id is an error. An issue with the above code is that if your application has multiple userdata types, then inspect_token can lead to undefined behavior -- it uses static_cast to convert any userdata type to a token. For this and other reasons, you should always set a metatable associated to any userdata value, and use luaL_testudata to test that it's the userdata type you think you had. Rather than illustrate that, we'll now show a more primer way of doing it.

struct token {
  int id;
};

namespace primer {
namespace traits {

template <>
struct userdata<token> {
  static constexpr const char * name = "token";
};

} // end namespace traits
} // end namespace primer


primer::result
new_token(lua_State * L) {
  static int count = 0;
  primer::push_udata<token>(L, count++);
  return 1;
}

primer::result
inspect_token(lua_State *, token & tok) {
  std::cout << "Token id: " << tok.id << std::endl;
  return 0;
}

  // ...

  lua_pushcfunction(L, PRIMER_ADAPT(&new_token));
  lua_setglobal(L, "new_token");
  lua_pushcfunction(L, PRIMER_ADAPT(&inspect_token));
  lua_setglobal(L, "inspect_token");

  const char * script =
    ""
    "x = new_token() "
    "y = new_token() "
    "inspect_token(x) "
    "inspect_token(y) "
    "y = x "
    "inspect_token(y)";
  // "inspect_token(5)";

    assert(LUA_OK == luaL_loadstring(L, script));
    assert(LUA_OK == lua_pcall(L, 0, 0, 0));

In this example, the userdata type token is passed by reference to the function inspect_token. This implicitly carries out the luaL_testudata, and signals an argument error if the it isn't passed a token. (i.e. inspect_token(5)) Generally userdata must be passed by reference and not by value -- the values exist in lua's memory, not on the stack. It's usually incorrect and undesirable to take a copy. By constrast, when passing std::string or std::vector, usually it makes the most sense to recieve it by value. It doesn't exist in that format in lua's memory, so primer will make a temporary when it calls your function. You might as well take ownership of that temporary, rather than getting it by const & or something.

Metatables

Extending our contrived example, let's suppose we want lua scripts to be able to access the token id, but not change it. We'd like to give token a metatable that looks like

{
  __index = impl_token_index
}

where impl_token_index is some appropriate C function. In pure lua it looks something like this:

struct token {
  int id;
};

int
impl_token_index(lua_State * L) {
  token & tok = *static_cast<token *>(luaL_checkudata(L, 1, "token"));
  if (0 == std::strcmp(lua_tostring(L, 2), "id")) {
    lua_pushnumber(L, tok.id);
    return 1;
  }
  return 0;
}

void
init_token_metatable(lua_State * L) {
  luaL_newmetatable(L, "token");

  lua_pushcfunction(L, impl_token_index);
  lua_setfield(L, -2, "__index");

  lua_pop(L, 1);
}

int
new_token(lua_State * L) {
  static int count = 0;
  void * udata = lua_newuserdata(L, sizeof(token));
  new (udata) token{count++};
  luaL_setmetatable(L, "token");
  return 1;
}

int
inspect_token(lua_State * L) {
  token & tok = *static_cast<token *>(luaL_checkudata(L, 1, "token"));
  std::cout << "Token id: " << tok.id << std::endl;
  return 0;
}

  // ...

  init_token_metatable(L);

  lua_pushcfunction(L, new_token);
  lua_setglobal(L, "new_token");
  lua_pushcfunction(L, inspect_token);
  lua_setglobal(L, "inspect_token");

  const char * script =
    ""
    "x = new_token() "
    "y = new_token() "
    "inspect_token(x) "
    "inspect_token(y) "
    "if x.id < y.id then print('x < y') end "
    "y = x "
    "if x.id == y.id then print('x == y') end ";

    assert(LUA_OK == luaL_loadstring(L, script));
    assert(LUA_OK == lua_pcall(L, 0, 0, 0));

This example prints

Token id: 0
Token id: 1
x < y
x == y

When using primer, some steps above become unnecessary. Particularly, init_token_metatable happens implicitly. It's necessary for primer to know how to set up the metatables properly -- basically it lets initialization happen lazily, while also allowing serialization and deserialization to happen with less work on your part.

struct token {
  int id;
};

primer::result
impl_token_index(lua_State * L, token & tok, const char * str) {
  if (0 == std::strcmp(str, "id")) {
    primer::push(L, tok.id);
    return 1;
  }
  return 0;
}

namespace primer {
namespace traits {

template <>
struct userdata<token> {
  static constexpr const char * name = "token";

  static constexpr std::array<luaL_Reg, 1> metatable{
    {{"__index", PRIMER_ADAPT(&impl_token_index)}}};
};

} // end namespace traits
} // end namespace primer


primer::result
new_token(lua_State * L) {
  static int count = 0;
  primer::push_udata<token>(L, count++);
  return 1;
}

primer::result
inspect_token(lua_State *, token & tok) {
  std::cout << "Token id: " << tok.id << std::endl;
  return 0;
}

  // ...

  lua_pushcfunction(L, PRIMER_ADAPT(&new_token));
  lua_setglobal(L, "new_token");
  lua_pushcfunction(L, PRIMER_ADAPT(&inspect_token));
  lua_setglobal(L, "inspect_token");

  const char * script =
    ""
    "x = new_token() "
    "y = new_token() "
    "inspect_token(x) "
    "inspect_token(y) "
    "if x.id < y.id then print('x < y') end "
    "y = x "
    "if x.id == y.id then print('x == y') end ";

    assert(LUA_OK == luaL_loadstring(L, script));
    assert(LUA_OK == lua_pcall(L, 0, 0, 0));

Usually, it is simplest for metatable to be a constexpr std::array like above. However you have other options, see the reference section for an exhaustive spec.


PrevUpHomeNext