Primer is designed with the following constraints in mind:
The main things that you must do to ensure good behavior are
push,
do not throw C++ exceptions. A lua error should only be raised in case
of lua memory allocation failure.
read,
do not throw C++ exceptions, or raise lua errors. If reading fails, return
a primer::error.
int(lua_State*)) that throw. Those functions should
handle problems internally or signal lua errors using the C api directly.
primer_result(lua_State
*, ...)
that throw. These functions should return a primer::error
if they fail, which will cause primer to raise a corresponding lua error.
These functions should only raise lua errors directly in case of lua
memory allocation failure. If this really cramps your style, consider
making a specialization of adapt
that catches some choice exceptions.
The two error scenarios that we consider here are:
We take as a given that it is not okay to throw foreign exceptions through lua, no matter how it is compiled. [1] [2] [3]
When a lua error occurs, what exactly happens is slightly different depending on whether lua is compiled as C or C++.
When lua is compiled as C++, lua API calls raise lua errors by throwing a C++ exception, which propagates back into lua and is caught there. In this case primer can use standard techniques to prevent problems.
When lua is compiled as C, the setjmp
and longjmp functions are
used to raise lua errors. The behavior is similar, but in this case, destructors
of any C++ automatic objects are not called. [4]
With careful practices, this problem can be largely avoided -- many C++ projects use lua compiled as C for one reason or another. [5]
Fortunately, many lua C api calls don't raise lua errors, except in the case of a lua memory allocation failure. [6]
On one hand, many projects that use lua are not concerned with the possibility
of a memory allocation failure, and are perfectly happy
with a lua_panic and program
termination in such scenarios. When they use the lua C API, these projects
will tend to be more fast-and-loose, not creating protected contexts to catch
such errors and so on.
On the other hand, some programs are concerned with memory allocation failure also, and we've decided that Primer should be engineered in a way that it can properly respond to such failures.
To handle the tradeoff between maximum performance and comprehensive error
handling, Primer responds to a special preprocessor define PRIMER_NO_MEMORY_FAILURE.
When this symbol is defined, Primer will assume that memory allocation failures will not occur, and use this assumption to skip handling of such errors in order to run faster. Specifically:
m
in the manual (see 4.8)
will not raise a lua error.
std::bad_alloc will not be thrown while
pushing or reading any standard type or container (std::string,
std::vector).
The rationale here is that lua is typically configured to use the same allocator as the rest of your program -- it's not that likely that lua is out of memory but the standard allocator is not, and there are few projects where the difference matters. [7]
We feel that this method of subtracting selected checks in order to go faster leads to a more reliable and maintainable technology, than if we had simply written Primer with no handling for potential memory allocation failures.
![]() |
Note |
|---|---|
This symbol is automatically defined if lua is compiled
as C. This decision allows Primer to be written in mostly idiomatic C++
code, because it means that we can assume that lua memory allocation failures
won't lead to |
![]() |
Caution |
|---|---|
If memory allocation failures do occur when this symbol is defined, undefined behavior will result. |
When lua is compiled as C++, and the symbol is not defined (the default),
Primer will handle all such errors internally using try
/ catch
and lua_pcall, without leaks
or undefined behavior. Generally it will signal errors along normal channels
to user code or to lua scripts in such cases.
The overhead of running with full protection is, roughly:
try
/ catch.
try
/ catch.
lua_ref requires
a pcall.
lua_ref::as requires a pcall.
lua_ref_seq
requires a pcall and
a try /
catch.
bound_function
or coroutine requires
two pcall instead of
one, plus a try /
catch if multiple return values
are requested.
Since Primer sits very close to lua and most of its functions could potentially be called by a lua callback, Primer doesn't throw any exceptions itself.
Internally, Primer handles errors using a class template expected<T>,
which is a union holding either a value or an error signal. When an operation
was requested by the user and fails, usually we return this expected<T> type
so that they get the error message. When a callback created with primer fails
after being called by lua, the error message is passed to lua and an error
is raised.
Since we cannot throw exceptions through lua, or handle the user's exceptions, we sometimes have very few options in how to cope with them, and in many cases the user is not permitted to throw errors into Primer.
Primer is mainly concerned with:
static_assert
to enforce that exceptions will not be thrown by user code at places
where we aren't prepared to handle them.
[8]
Note that this philosophy is different from how some C++ lua binding libraries work. Many of them have various ways of trying to handle the user's exceptions and either report them to lua as errors or try to get them across lua by various schemes without actually throwing them through lua.
We consider that exceptions should only be used for very serious problems,
and that if one is thrown, it is crucial that it reaches the handler you
wrote in your program. Any of these mechanisms that blindly translate exceptions
into lua errors can potentially lead to the exception being lost and swallowed
if lua scripts are using pcall
to call your functions, and then it becomes very difficult to reason about
what happens next. So we don't plan to do something like this generically
in Primer. We think it is a better idea to terminate the program in such
cases. [11]
Instead, we are mainly concerned with avoiding leaks and indeterminate states, and preempting exceptions in places where we can't handle them.
push:
read:
primer::error
in case of failure.
primer::expected only supports objects
which are nothrow_move_constructible,
so only such objects can be read.
This is enforced via static_assert.
nothrow_constructible.
This is enforced via static_assert.
nothrow_move_assignable,
but this is not required, it will only go slower if they
are not.
primer::result(lua_State
*, ...)
then it must not throw. You must add a custom specialization of
adapt if you want
it to catch your exceptions.
[1] c.f. lua mailing list
[2] c.f. more from lua mailing list
[3] c.f. lua users wiki
[4] Usually this would lead "only" to a memory leak, but actually the standard says that undefined behavior results if an object with a nontrivial destructor is jumped over.
[5]
Primer helps with this significantly because when adapting callbacks for
lua, you signal lua errors and yield calls via a return value rather than
by calling lua functions which longjmp
directly. Primer calls the longjmp
itself, after you have returned and your local objects have already been
cleaned up.
[6]
See 4.8
in the lua manual. The notation [-o, +p, x] is there explained: -
means the function never raises any error; m
means the function may raise out-of-memory errors and errors running a
__gc metamethod. For purposes
of this discussion, we are going to ignore "errors running a __gc metamethod". See also here and here
in the manual.
[7]
Actually you can work around dependence on the standard allocator by specializing
push and read for standard types std::string, std::vector,
etc. with custom allocators.
[8]
The applicability of static_assert
is unfortunately limited. For some simple things, it is helpful, but for
callback functions, it is problematic. When lua is compiled as C, then
a lua error is a longjmp,
so a function which throws no exceptions and may raise lua errors can be
marked noexcept. When lua
is compiled as C++, lua errors are exceptions, so noexcept
cannot be used.
[9]
For instance, luabind attempts
to make lua exception-safe in a limited sense -- they
put a wrapper over all functions that you pass to lua through their interface,
which catches all exceptions derived from std::exception, and raises a lua error to
handle them, passing the e.what() string to lua as the error message.
[10]
Another strategy that I've seen is to stash the exception object away in
some static storage, pass on the signal to lua as a lua error, and rethrow
the exception on the other side, once the error presumably propagates out
to a pcall somewhere.
[11]
If you want to create an exception-to-lua-error translation mechanism specific
to your program, you are welcome to specialize the adapt
trait in order to do it, and it's not too difficult. See the documentation
for adapt.