Primer contains a handy utility called cpp_pcall
which can be used to execute any C++ call with any arguments inside of
a lua protected context.
This is essentially an extension of the cpcall
idiom described on the lua mailing lists, except using C++ generic programming
techniques.
For reference, see this
description and example of lua_cpcall
usage.
int operate(lua_State * L, std::string & s, int x, int y) { std::string msg = "Calling " + s + "\n"; // can raise exception; must be destroyed cout << msg; // caution: this code by raise exceptions but not longjump. struct C { static int call(lua_State * L) { // caution: this code may longjump but not raise exceptions. C * p = static_cast<C*>(lua_touserdata(L, 1)); assert(lua_checkstack(L, 4)); lua_getglobal("add"); // can longjump assert(lua_isfunction(L, -1)); lua_pushstring(L, s); // can longjump lua_pushnumber(L, p->x); lua_pushnumber(L, p->y); lua_call(L, 3, 1); // can longjump p->z = lua_tonumber(L, -1); assert(lua_isnumber(L, -1)); return 0; } const char * s; int x; int y; int z; } p = {s.c_str(), x, y, 0}; int res = lua_cpcall(L, C::call, &p); // never longjumps if (res != 0) { handle_error(L); // do something with the error; can raise exception //note: we let handle_error do lua_pop(L, 1); } return p.z; }
In summary, the idea is to create a local struct
that will hold pointers to the call parameters, then do something with
them in a lua callback, then, push that callback and a pointer to the structure
to pcall.
cpp_pcall accomplishes
the same but without the boiler-plate. In addition, cpp_pcall
can work not only with a function pointer, but any C++ function object
or lambda function, which often makes this whole idiom much nicer.
namespace primer {template <int narg = 0, typename F, typename... Args> expected<void> cpp_pcall(lua_State * L, F && f, Args &&... args) noexcept; }
|
Takes a callable and some arguments, performs the call in a protected context which captures lua errors.
Usually,
The first parameter to
The optional template parameter The callable itself must not throw a C++ exception, or terminate will be called. |
A related function, mem_pcall,
is used internally by the Primer to capture lua memory errors as appropriate
according to the value of PRIMER_NO_MEMORY_FAILURE.
Sometimes, we perform operations which can raise lua errors, but only of
the memory variety. In this case, we call mem_pcall
instead of cpp_pcall, which
is resolves to either a cpp_pcall
or a simple invocation of the callable, depending on the PRIMER_NO_MEMORY_FAILURE define. This,
together with PRIMER_TRY_BAD_ALLOC
/ PRIMER_CATCH_BAD_ALLOC,
is the total effect of the PRIMER_NO_MEMORY_FAILURE
switch. mem_pcall should not be used with calls that can throw an exception.
It also should not be used with calls that can raise lua errors of the
non-memory variety, since when PRIMER_NO_MEMORY_FAILURE
is defined the protection is stripped out. If you need full protection
then use cpp_pcall directly.
template <int narg = 0, typename F, typename... Args> expected<void> mem_pcall(lua_State * L, F && f, Args &&... args) noexcept { #ifdef PRIMER_NO_MEMORY_FAILURE static_cast<void>(L); std::forward<F>(f)(std::forward<Args>(args)...); return {}; #else return cpp_pcall<narg>(L, std::forward<F>(f), std::forward<Args>(args)...); #endif }
For instance, here's an example from the implementation of bound_function. protected_call
is a member function of bound function which is used to implement all of
its forwarding facing functions.
// Calls the bound_function in a protected context. This is no fail. template <typename return_type, typename... Args> expected<return_type> protected_call(Args &&... args) const noexcept { expected<return_type> result{primer::error::cant_lock_vm()}; if (lua_State * L = ref_.lock()) { if (auto stack_check = detail::check_stack_push_each<int, Args...>(L)) { auto ok = mem_pcall(L, [&]() { ref_.push(L); primer::push_each(L, std::forward<Args>(args)...); detail::fcn_call(result, L, sizeof...(args)); }); if (!ok) { result = std::move(ok.err()); } } else { result = std::move(stack_check.err()); } } return result; }
In this code, the lambda contains calls that could potentially cause a
lua memory failure, when pushing objects and also popping return values
off of the stack. mem_pcall
allows us to catch all such errors, and when PRIMER_NO_MEMORY_FAILURE
is defined, the mem_pcall
and the lambda are inlined, producing a direct call with no overhead.
For the curious, cpp_pcall's
implementation looks like this:
namespace detail { template <typename T> int lambda_upvalue_dispatch(lua_State * L) { T * t = static_cast<T *>(lua_touserdata(L, lua_upvalueindex(1))); (*t)(); return lua_gettop(L); } } // end namespace detail template <int narg, typename F, typename... Args> expected<void> cpp_pcall(lua_State * L, F && f, Args &&... args) noexcept { expected<void> result; auto lambda = [&]() { (std::forward<F>(f))(std::forward<Args>(args)...); }; lua_pushlightuserdata(L, static_cast<void *>(&lambda)); lua_pushcclosure(L, &detail::lambda_upvalue_dispatch<decltype(lambda)>, 1); if (narg) { lua_insert(L, -1 - narg); } int code; // pcall_helper installs a custom error handler std::tie(code, std::ignore) = detail::pcall_helper(L, narg, LUA_MULTRET); if (code != LUA_OK) { result = pop_error(L, code); } return result; }