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.
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.