PrevUpHomeNext

Exception Safety

Primer is designed with the following constraints in mind:

The main things that you must do to ensure good behavior are

Design Considerations

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.

Goals

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:

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] 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 longjmp.

[Caution] 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:

Handling of Errors

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:

  1. Basic exception safety:
    1. Don't leak memory / objects. Don't put objects in an indeterminate state.
    2. Don't leak lua resources, e.g. fail to clean up objects on the stack.
  2. Fail fast:
    1. If an exception will reach lua, it is better to terminate the program.
    2. If possible, use static_assert to enforce that exceptions will not be thrown by user code at places where we aren't prepared to handle them.

[8]

Comments

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.

[9] [10]

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.

Exception Safety Requirements

  1. User specializations of push:
    • Must not throw C++ exceptions.
    • May raise a lua error only in case of lua memory allocation failure.
  2. User specializations of read:
    • Must not throw C++ exceptions.
    • Should return primer::error in case of failure.
    • Note: primer::expected only supports objects which are nothrow_move_constructible, so only such objects can be read. This is enforced via static_assert.
    • With regards to containers:
      • Arrays and visitable structures must further be nothrow_constructible. This is enforced via static_assert.
      • Elements in arrays and visitable struture should be nothrow_move_assignable, but this is not required, it will only go slower if they are not.
  3. Userdata
    • May throw from constructor.
    • Must not throw from destructor.
  4. When adapting a C++ function for lua:
    • If it has signature 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.


[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.


PrevUpHomeNext