Now, we'll show how userdata works in conjunction with serialization, by
bringing back the token
userdata type. The main thing that's new is, we need to register the userdata
using an API feature, and we need to provide a __persist
metamethod.
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; } // "ctor" for token type, constructs it from an int, passed as first up-value. // This isn't part of our API directly, but it's needed for persistence primer::result token_ctor(lua_State * L) { int id = luaL_checkinteger(L, lua_upvalueindex(1)); primer::push_udata<token>(L, id); return 1; } // persistence method // Given a token, it pushes a lua closure which *yields* the // same token back when called. Eris can't serialize the token // directly, but it can serialize this closure, because it knows how to serialize // the `int` up-value, and `token_ctor` goes into the permanent objects table. primer::result impl_token_persist(lua_State * L, token & tok) { primer::push(L, tok.id); lua_pushcclosure(L, PRIMER_ADAPT(&token_ctor), 1); return 1; } // userdata declaration, with both metatable and permanent ojects namespace primer { namespace traits { template <> struct userdata<token> { static constexpr const char * name = "token"; static constexpr std::array<luaL_Reg, 2> metatable() { return {{{"__index", PRIMER_ADAPT(&impl_token_index)}, {"__persist", PRIMER_ADAPT(&impl_token_persist)}}}; } static constexpr std::array<luaL_Reg, 1> permanents() { return {{{"ctor", PRIMER_ADAPT(&token_ctor)}}}; }; }; } // end namespace traits } // end namespace primer struct lua_raii { lua_State * L_; lua_raii() : L_(luaL_newstate()) {} ~lua_raii() { lua_close(L_); } operator lua_State *() const { return L_; } }; namespace api = primer::api; struct my_api : api::base<my_api> { lua_raii lua_; API_FEATURE(api::callbacks, callbacks_); API_FEATURE(api::persistent_value<int>, count_); API_FEATURE(api::userdatas<token>, userdata_types_); NEW_LUA_CALLBACK(new_token)(lua_State * L)->primer::result { primer::push_udata<token>(L, count_.get()++); return 1; } NEW_LUA_CALLBACK(inspect_token)(lua_State *, token & tok)->primer::result { std::cout << "Token id: " << tok.id << std::endl; return 0; } my_api() : lua_() , callbacks_(this) , count_{0} { this->initialize_api(lua_); } void run_script(const char * script) { lua_settop(lua_, 0); if (LUA_OK != luaL_loadstring(lua_, script)) { std::cerr << lua_tostring(lua_, -1); std::abort(); } if (LUA_OK != lua_pcall(lua_, 0, 0, 0)) { std::cerr << lua_tostring(lua_, -1); std::abort(); } } std::string serialize() { std::string result; this->persist(lua_, result); return result; } void deserialize(const std::string & str) { this->unpersist(lua_, str); } }; // Test that we can make two separate, encapsulated copies of the api object, // and serialize and deserialize them. int main() { my_api api1; api1.run_script( "x = new_token() " "y = new_token() " "inspect_token(x) " "inspect_token(y) "); my_api api2; api2.run_script( "z = new_token() " "x = new_token() " "inspect_token(x) "); api1.run_script("inspect_token(x)"); std::string s = api1.serialize(); api2.run_script("inspect_token(x)"); api2.deserialize(s); api2.run_script("inspect_token(x)"); }
There are six calls to inspect here: they print respectively
Token id: 0 Token id: 1 Token id: 1 Token id: 0 Token id: 1 Token id: 0
The two new functions we have which we didn't have before are the token_ctor and the persist
implementation. token_ctor
is like a constructor or factory function for token's,
but adapted for lua.
Notably, it takes it's argument from lua_upvalueindex(1)
rather than from the stack. This is because it is always used as a closure.
When eris serializes the lua state and encounters a token
userdata, it inspects the __persist
metamethod. Since the value is a function, it calls that function, expecting
to find a closure object which will be serialized. That closure object,
when called, yields the original token
userdata value.
This approach reduces the problem of serializing userdata to serializing a closure. To serialize a closure, we need to be able to serialize the function, and all the up-values.
We can serialize the function because token_ctor
is in the permanents list. If it wasn't, we would get a runtime error on
failing to serialize the function. Note that nothing says we have to use
a "C closure", we could concievably use a function written in
lua rather than a function written in C++ -- the lua closure could concievably
call other parts of our API, since it needs to somehow produce a new userdata
value. Eris can serialize functions written in lua just fine, without any
assistance. In the typical case, though, it's easier for these things to
be implemented in C++ and to just put them in the permanent object's table.
We can serialize the up-values here because it's just an integer. In general,
the up-values could be userdata also, and then the process would be repeated
recursively. Eventually, every table, closure, or other composite object
gets broken down into either primitive values which can be directly serialized,
or into values in the permanent objects table. When deserializing, eris
requires you to provide the "reverse" permanent objects table,
which maps the "stand-in" values back to the "true"
values. When using primer,
you don't have to do this explicitly, it does it automatically based on
what API features are registered and what permanent objects they declare.
One thing other thing you might notice in the above code -- in earlier
examples we used a static constexpr data member metatable
in the userdata declaration. In this example, we used a static
constexpr function instead. Primer
is okay with either form, but sometimes you need to use a function for
the program to link. (If I understand right, the standard doesn't actually
say that the first version should link until C++17, due to changes in the
ODR, and the introduction of "inline variables", but gcc and
clang mostly do anyways...) The function format conceptually slightly less
clear perhaps, but it's usually better and more useful. For instance, it
allows for better encapsulation in common cases, when you want to declare
a userdata type in the header, and specialize the trait there, and conceal
all the method implementations in a cpp file.