A class which matches the "API Feature" concept represents some functionality that we can expose to a lua VM, together with the info needed to serialize it.
An object matching the API feature concept has the following three methods:
struct my_feature { void on_initialize(lua_State *); void on_persist_table(lua_State *); void on_unpersist_table(lua_State *); };
The on_initialize method
is called when the API is initialized.
The on_persist_table method
is called when we are persisting the state, and need to construct the permanent
objects table. When called, your feature will get a lua_State
*, and can assume that the permanent
objects table is on top of the stack. It should register each permanent
object, using the object as the key, and (usually) a serialization token
as the value.
The on_unpersist_table
method is the same, but it does the reverse. The key should be the token,
and the value should be the object.
Usually, permanent objects are just function pointers, but occasionally
they might be something else. For instance if there is a global userdata
of some type, it could be serialized by putting it in the permanent objects
table, rather than providing a __persist
metamethod.
Sometimes, the private state of an API feature is important, and you want to have an easy way to serialize that state along with the rest of the lua state. In this case you can provide the following two additional methods:
void on_serialize(lua_State *); void on_deserialize(lua_State *);
These two methods hook into the persistence process in a different way -- instead of adjusting the permanent objects table, they add a value to the target table.
When on_serialize is called,
the feature should push one lua value onto the stack which will be associated
to it and saved. It may be a complex table or any other thing that primer
is able to serialize. Exactly one value should be pushed.
When on_deserialize is
called, the feature may recover the lua value from the stack, which it
will find on top. It should use the value to restore its internal state,
then pop the value.
A feature which also has these two methods is called a serial feature.
When api::base::persist is called, the permanent objects
table is constructed by calling on_persist_table
for each API feature, and the target table is constructed using _G, and each of the values
provided by an API feature which has an on_serialize
method.
When api::base::unpersist is called, the reverse-permanent
objects table is constructed first using on_unpersist_table,
and then the target table is reconstructed from the given string. Then,
each API feature with an on_deserialize
method is passed back the object which it pushed onto
the stack, in order to restore its internal state. And the reproduced image
of _G replaces the current
value of _G.
For example, a feature that represents a custom lua library should install
the library into the lua state when on_initialize
is called.
When on_persist_table is
called, it should add each function pointer and a name for it, using the
function pointer as a key, to the given table (assumed to be on top of
the stack).
When on_unpersist_table
is called, it should do the same, but in reverse, putting the name as the
key.
struct my_functions { static const std::array<luaL_Reg, 3> get_funcs() { return {{"foo", &foo}, {"bar", &bar}, {"baz", &baz}}; } void on_init(lua_State * L) { lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); primer::set_funcs(L, get_funcs()); // ^ similar to luaL_setfuncs } void on_persist_table(lua_STate * L) { primer::set_funcs_reverse(L, get_funcs()); } void on_unpersist_table(lua_STate * L) { primer::set_funcs(L, get_funcs()); } };
A different feature might use these methods in a totally different way.
For instance, suppose your api requires having a single unique userdata object which is globally visible to scripts and serves some important purpose.
The on_initialize method
might install this object, and also store a hidden reference to it in the
lua registry so that you can find it again.
The on_persist_table method
might fetch the reference from the registry and map that object to some
name "my_awesome_object".
Then in the unpersist table, you can map "my_awesome_object"
back to the new instance of that object in a recreated VM.
An API feature is a data member of the API object. This allows it to have private variables, nontrivial initialization, links to other objects in your C++ program, and for any methods that it install in the lua state to have potentially have additional useful side-effects in your program.
Examples of functionalities that Primer has built-in API Features for include
loadfile,
dofile, require)
However, anything matching the concept can be used -- the methods are just hooks that you can use into the serialization process in an organized, object-oriented manner.
To assert that a type declares the proper member functions to be an API
feature, you can use a static_assert
like this:
static_assert(primer::api::is_feature<T>::value, "T does not match the API_FEATURE concept!");
To assert that it further is a "serial feature" containing the
on_serialize and on_deserialize methods, you can use the
is_serial_feature trait:
static_assert(primer::api::is_serial_feature<T>::value, "T does not match the serial feature concept!");
If a type is recognized as a feature but not a serial feature, because
it has only one of on_serialize
and on_deserialize, it
will just be silently used as an API feature, so for debugging purposes
the above assertion is sometimes useful.