PrevUpHomeNext

Design

Primer is a header-only C++ library. It is intended to be a thin layer over lua/eris. As such, we don't make any use of "heavy" C++ features like virtual functions, exceptions, or RTTI.

We do however make heavy use of templates. Primer's headers generate many tiny functions which, in an optimized build, the compiler will inline (god willing).

It is recommended also to statically link to lua or lua-eris, as for current compilers, this is the best way to take advantage of link-time optimizations.

Primer is also intended to be light-weight in another more important way -- it should be cohesive with the lua C API. Mechanisms like primer::read, primer::push, and primer::adapt, are meant to be readily understandable as a short series of calls to the lua C API following a pattern. Primer is meant to provide tools which supplement the lua C API, not to replace it, and to provide organizational structures that facilitate its use. We aren't trying to "abstract away the lua stack" or anything like that here.

Error Handling

primer::error is the basic object which Primer uses to represent a runtime error. Primer translates lua errors into primer::error when it performs an operation which fails, and will translate primer::error into a lua error when adapting callbacks.

primer::expected<T> is a class template which Primer uses to perform error handling. This is a discriminated union type, representing either a T value, or a primer::error. This template is broadly similar to the std::expected which was proposed for C++17, except that it does not use exceptions.

Reading and Pushing

primer::read and primer::push are template functions which form the foundation of primer. These two functions establish how to transfer C++ values to and from the lua stack. They have the following signatures:

template <typename T>
void push(lua_State * L, const T & t);

template <typename T>
expected<T> read(lua_State * L, int index);

These functions should not be overloaded or specialized. Instead, both functions are backed up by a type trait, which may be specialized to customize primer's behavior.

Many other functions in primer's repertoire use the push and read traits implicitly, so specializing these makes primer universally "aware" of your custom types.

Traits are used because it provides for more precise and powerful customization. For instance, if we merely used function overloading, then all kinds of implicit conversions can come into play, and your ability to provide partial specializations is limited.

Reflecting C++ into lua

Adapting classes as userdata types

Userdata types are registered by specializing a trait, primer::traits::userdata.

The specialized trait is required to provide enough information to construct the metatable of the userdata.

While primitive types like numbers and tables are often read by value, using e.g. primer::read<float> or primer::read<std::vector<int>>, generally it is much more useful to read userdata types by reference.

If my_type has been registered as userdata, then primer::read has a built-in specialization for the type my_type &. If a userdata of type my_type is found at that stack position, then a reference to the underlying my_type is returned, otherwise, an error.

primer::push_udata is used to create new userdata on the stack of a given type. It forwards all arguments to the constructor of the userdata type.

Adapting functions

PRIMER_ADAPT is the mechanism which implements type-safe parameter reading and error handling.

It takes a function pointer of a general signature, and constructs a delegate of the form lua_CFunction which can be pushed to lua.

PRIMER_ADAPT uses primer::read under the hood. If you specialize the read trait for a special type or container, then that type or container can be used as a parameter in any lua callback which you expose using primer. A reference or const reference to any userdata type can also be used as a function parameter, since primer::read recognizes it.

Adapting member functions

PRIMER_ADAPT only directly handles free functions. In order to dispatch calls to member functions, we must construct a delegate which can recover the pointer to the base object by some means.

Primer provides three different ways to do this. They each work by recovering this pointer, then using PRIMER_ADAPT to read arguments from the lua stack and call the target function. The three different mechanisms are:

Each has their pros and cons.

userdata dispatch is used for userdata methods. This is simple and it works -- a drawback is that it imposes syntactic constraints on your lua API, as the userdata methods must be invoked as object methods.

extraspace dispatch is used for callbacks in an API object. In this method, member functions of the API object are exposed as free functions in lua -- from the point of view of the script, they are not attached to any object. But in their implementation, they have access to data members of the API object associated to the lua state. extraspace dispatch works by storing a pointer in the lua extraspace region. It may be significantly faster than other dispatch methods. But, only one object may have its members dispatched this way. It is only appropriate for "global" functionalities that your API exposes.

std::function dispatch is a third method. In an extra header, primer provides the ability to push any std::function object to lua. This mechanism is very flexible, but it comes with all the caveats of using std::function -- you must make sure that any pointers concealed inside it are not left dangling, and you pay some price in overhead for using the std::function. Also, std::function is usually impossible to serialize. This dispatch mechanism is not usually appropriate to use when you are hoping to use eris to serialize your lua state, it is only provided for convenience in general lua applications.

These are viewed as low-level decisions -- usually, the choice is obvious from the context and primer does this for you. However, it is not difficult to access or modify the three different mechanisms directly if you want to.

Reflecting lua into C++

Lua values

primer::lua_ref is a lightweight C++ object which represents a reference to a particular lua value inside of some VM.

First, the value is pushed on top of the stack. Then, lua_ref is constructed, which pops the values and stores a link to it in the registry.

The value can later be retrieved by using the lua_ref::push() function, which pushes it back on top of the stack. This only fails if the lua VM has already been destroyed, and this is safe in the sense of producing a failure signal rather than undefined behavior.

You can simply convert the value to a C++ value by using the lua_ref::as function, which uses lua::read to try to convert it. This is stack-neutral.

if (expected<int> i = ref.as<int>()) {
  std::cout << "The integer is '" << *i << "'\n";
}
Lua functions

Some lua values cannot be converted to C++ values.

primer::bound_function is a primer::lua_ref which is known to point to an object of function type. It has extra member functions in order to support calling the function easily from C++. A primer::bound_function can be called by passing it C++ objects (which it passes to lua using primer::push), and it can return the results as e.g. expected<lua_ref> or similar.

This makes it easy to construct typical-looking C++ function objects which actually call lua functions.

struct my_function_object {
  primer::bound_function func_;

  int call(int input) {
    if (auto result = func_.call_one_ret(input)) {
      if (auto maybe_int = result->as<int>()) {
        return *maybe_int;
      } else {
        // The lua function did not return an integer
        throw std::runtime_error(maybe_int.err_str());
      }
    } else {
      // The function call failed
      throw std::runtime_error(result.err_str());
    }
  }
};
Lua coroutines

primer::coroutine is a primer::lua_ref which represents a lua coroutine. It serves a similar purpose as primer::bound_function, providing a safe and easy-to-use interface.

Constructing APIs

primer::api::base is the base type which you use to construct an API.

It has two special features:

The primer::api namespace contains many additional API feature objects which may be useful.

name

functionality

callback_manager

Sets up the extraspace dispatch and moves API callbacks into the global table.

userdata_manager

Registers a list of userdata types with the state, and makes sure their permanent objects go in the permanent objects table.

library_manager

Registers a subset of the standard lua libraries with the state, and makes sure their functions go in the permanent objects table.

print_manager

Sets up a custom channel where print signals from lua will go, and provides an interface for a temporary "interpreter" session useful for debugging.

vfs

Sets up a custom vfs to which functions like loadfile, dofile, and require become attached, replacing the default implementations which access the filesystem directly.

In order for member functions to be registered with the lua state, there must be a primer::api::callback_manager which is explicitly declared as an API_FEATURE within the api. This callback manager is initialized using the this pointer of the api::base. On initialization, it stores the this pointer in the lua extraspace, and loads all of the methods into the global table, and into the permanent object tables at appropriate times.


PrevUpHomeNext