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.
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.
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.
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.
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.
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:
userdata dispatch
extraspace dispatch
std::function dispatch
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.
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"; }
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()); } } };
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.
primer::api::base
is the base type which you use to construct an API.
It has two special features:
API_FEATURE.
Such objects are expected to provide on_init, on_persist_table, on_unpersist_table
methods, and optionally, on_serialize
and on_deserialize. This
allows the function object to declare permanent objects, and provide
additional serialized data besides what is visible in the global table.
NEW_LUA_CALLBACK.
The primer::api namespace contains many additional
API feature objects which may be useful.
|
name |
functionality |
|---|---|
|
|
Sets up the extraspace dispatch and moves API callbacks into the global table. |
|
|
Registers a list of userdata types with the state, and makes sure their permanent objects go in the permanent objects table. |
|
|
Registers a subset of the standard lua libraries with the state, and makes sure their functions go in the permanent objects table. |
|
|
Sets up a custom channel where |
|
|
Sets up a custom vfs to which functions like |
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.