PrevUpHomeNext

Concepts

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.

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.

Permanent Objects

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

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:

  1. Literal persistence. Essentially, we just memcpy the object.
  2. No persistence. Persisting this object is a runtime error.
  3. Persistence using a closure.

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.

Primer

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:

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.


PrevUpHomeNext