PrevUpHomeNext

API Feature

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.

Extension: Serial Feature

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.

Behavior

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.

Examples

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

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.

Other

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.


PrevUpHomeNext