Everything in the api namespace
is organized around the goal of creating lua VMs which not only have features
which may be implemented in C++, but also which we can serialize
and deserialize seamlessly.
The way that we do this is using lua-eris.
The api::base object provides an interface which
encapsulates the use of eris.
When eris serializes a lua value, it produces a string description of it, from which an equivalent object can be constructed in another VM. This object can be anything from a simple lua value, to a complicated, self-referential table, containing complex closure functions and coroutines and so on.
Usually when you serialize a lua state, you just want to serialize the
_G table, which represents
the global environment visible to the user scripts.
Eris can serialize any object described purely in the lua language just fine. But for some objects, like external C function pointers, or userdata representing C++ objects, it needs some help.
The organizing idea is eris is the permanent objects table. When eris serializes a value, it does so relative to a fixed table, which maps such "unserializable" entities like function pointers to some placeholder names for them.
Then, when deserializing, it must be provided with the "inverse" of the permanent objects table, which maps those placeholder names back to the appropriate function pointers (potentially now on a different machine) and so on.
The placeholder values don't have to be "strings". They can be anything that eris is able to serialize -- even recursively, using the permanent objects table. This is sometimes very useful as we'll see.
Userdata is handled in a slightly different way -- by creating a special
metamethod called __persist.
See eris'
documentation on the subject.
There are three options:
memcpy
the object.
In C++, it's very common to have nontrivially copyable types as userdata.
In that case, the "literal persistence" is not an option, and
the third option is the most useful. What it means is that, for each userdata
type, there should be some C++ function adapted for lua which can act as
a "constructor". It doesn't need to be globally exposed to the
user, but it needs to appear in the permanent objects table. Then, for
any given state of an object, it needs to be possible to create a series
of arguments for that constructor which will produce an instance of the
object in that state. The __persist
metamethod is supposed to create those arguments for a given object, form
a closure over these using the constructor function (use the call lua_pushcclosure), and return that to
lua.
Then, when eris encounters your userdata type, it calls the __persist method, which produces the
closure object. It then tries to persist the closure object in place of
your userdata -- because the constructor is in the permanent objects table,
this succeeds, as long as all the upvalues that you closed over are also
serializable.
In Primer, the idea is to simplify all this by automating the construction of the permanent objects table.
When you do it the Primer way, for each lua State that you plan to serialize, there should be an associated API Object with which all of the components of that API are registered. The API object manages the processes of initializing the API within a State, and later, serializing and deserializing the state.
An API object should derive from the primer::api::base class template, using CRTP style:
struct my_api : primer::api::base<my_api> { ... };
This accomplishes two things:
API_CALLBACK and
API_FEATURE macros
may be used to register different members of the API object as being
parts of the API.
primer::api::base:
void initialize_api(lua_State *); void persist(lua_State *, std::string & buffer); void unpersist(lua_State *, const std::string & buffer);
What exactly the API object does when these methods are called depends
on what things get registered. Basically it's going to carry out the lua-eris serialization pattern in a straightforward
way, providing each API_FEATURE
object with simple hooks into that process so that it can participate.
The assumption here is that there is a single lua_State
* object associated with the API
object and you use that lua state each time you call these methods. So
usually the API object should own / manage the lifetime of the lua_State*.
However, we don't want to force you to do that in any particular way.
[13]
Conceptually, the api::base is just a place where you register
objects and functions. The actual class has no data members and no nontrivial
initialization or destruction, so inheriting from it does not conceptually
complicate your class. It just gives you some nice methods that you can
use as you like.
Primer provides you with many useful API_FEATURE
definitions for things like installing standard libraries, registering
your userdata types, and customizing the VM in various common ways. It's
quite easy to customize the system though, and make API features that do
whatever you want.
[13]
You could do it manually, using some RAII object, or maybe you are using
some other binding library. For instance in the lua binding library
sol, they provide you
with an RAII object called sol::state
which they want you to use. You could use lua in any manner like that
with Primer just fine, provided that you can obtain a raw lua_State *
from whatever the object is.