lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


On Fri, Mar 12, 2010 at 6:09 AM, M Joonas Pihlaja wrote:
> The safest approach is one where you keep a strict separation between
> contexts where Lua may raise an error and ones where C++ code may
> throw an exception, and never let the two overlap.

Yes, keeping the two contexts completely separate is key for
correctness.  Unfortunately, this can be difficult to do completely,
and I would hope that Lua 5.2 makes improvements here (actually it
does somewhat, as discussed below).

Difficulties abound particularly in extricating the small remaining
overlap that occurs when passing data between the two contexts.  If I
have a C++ string on the stack (exception context) and want to do Lua
operations on it (longjmp context), I need to first convert it to a
Lua string via lua_pushstring, but lua_pushstring is a longjmp context
operation.  I can, however, execute lua_pushstring and other longjmp
context operations within a lua_cpcall, which is safe under exception
context.  On the other hand, if I now want to pass the Lua string back
from longjmp context to exception context, I'll need to do this within
the lua_cpcall since this function does not return Lua values (except
on error).  So, I'll need to within the lua_cpcall temporarily switch
into exception context by doing a try/catch (which is safe inside
longjmp context), and write to a C++ string.

If this sounds like fun, it's not:

=====
// Example of "exception safe" concatenation of two strings in Lua from C++.
// D.Manura, 2010-03-12

#include <lua.hpp>
#include <iostream>
#include <string>
#include <stdexcept>

// Lua state wrapper.
struct LuaState {
  LuaState() {
    L = luaL_newstate();
    if (!L) { throw std::runtime_error(":P"); }
  }
  ~LuaState() { lua_close(L); }
  lua_State * L;
};

// RAII - ensures invariant that Lua stack size on destruction is same
// as on construction.
class LuaStackProtect {
public:
  LuaStackProtect(lua_State * L) : m_L(L), m_top(lua_gettop(L)) { }
  ~LuaStackProtect() { lua_settop(m_L, m_top); }
private:
  lua_State * m_L;
  int m_top;
};

int main() {
  // <no context>

  try {
    // <exception context>

    LuaState l;
    lua_State * L = l.L;

    // ensure Lua stack cleanup on exception--not actually necessary here
    // since the Lua state is immediately closed afterward, but in general
    // can be required.
    LuaStackProtect stk(L);

    std::string sa = "a";
    std::string sb = "b";
    std::string sc;
    struct C {
      static int call(lua_State * L) {
        // <longjmp context>

        C * cdata = (C*)lua_touserdata(L, 1);
        lua_pushstring(L, cdata->pa);
        lua_pushstring(L, cdata->pb);
        lua_concat(L, 2);

        // now return value back to exception context.
        // note: The following try/catch can be avoided if we replace
        // lua_cpcall with a lua_pcall given a Lua 5.2 style
        // cpcall function because lua_pcall can return values.
        bool ok = true;
        try {
          // <exception context>
          *(cdata->psc) = lua_tostring(L, -1);
        }
        catch(std::bad_alloc const & e) {
          // <no context>
          // note: not longjmp context because the exception
          // object `e` may need to be destroyed.
          // Implies luaL_error would not be safe directly from here,
          // which is unfortunate if you need e.what() since
          // e.what() goes out of scope and copying it could fail.
          ok = false;
        }
        if (!ok) luaL_error(L, "bad alloc");
          // LUA_ERRMEM may be more appropriate here,
          // but that is not exposed to the C API.  Should C++ allocation
          // failures be reported as Lua allocation failures?  It might be
          // confusing if they use two different allocators.
        return 0;
      }
      char const * pa;
      char const * pb;
      std::string * psc;
    } cdata = {sa.c_str(), sb.c_str(), &sc};

    if (lua_cpcall(L, C::call, &cdata) != 0) {
      // note: LuaStackProtect will remove the error value from the stack
      throw std::runtime_error(lua_tostring(L, -1));
    }
    std::cout << sc << std::endl;
  }
  catch (std::exception const & e) {
    std::cerr << e.what() << std::endl;
    return 1;
  }

  return 0;
}
=====

I suppose the new closure support in c++0x would improve this somewhat
(i.e. the "struct C").  Lua 5.2 does at least make a change that more
efficiently executes cpcall's without memory allocation.  You can even
replace the lua_cpcall with a Lua 5.2 style cpcall called from a
lua_pcall, which pushes Lua return values on the stack, even safely in
exception context, thereby avoiding the ugly inner try/catch above.
This stuff was suggested in the related discussion thread "is it
possible to make longjmp-free Lua?" [1], which also expresses many
similar concerns and notes that sometimes it would help if Lua could
return an error code rather longjump.  Juris Kalnins shows [2] there
that the concern is not only in the context of C++, and there was an
interesting solution of unwrapping the lua_cpcall to avoid the closure
[3].

[1] http://lua-users.org/lists/lua-l/2009-07/threads.html#00380
[2] http://lua-users.org/lists/lua-l/2009-07/msg00387.html
[3] http://lua-users.org/lists/lua-l/2009-08/msg00227.html


> You may opt to target Lua VMs which are compiled to use your
> compiler's C++ exceptions instead of setjmp/longjmp error handling,...
> Looking at the code inluaconf.h, it looks like Lua does not extract
> any sensible error object from exceptions which it didn't raise itself.

That would seem to simplify things.  However, even apart from the
possible issue you mention (see also [3]), I haven't thought this
would be a practical solution in the general case since globally
enabling exception semantics would interfere with loading binary
modules compiled under some different compiler or C.  I think it's
most practical to think of Lua as a C library unless maybe if you have
a private instance of Lua that you're certain doesn't interface to C
code.  I would not be opposed though to having Lua provide exception
semantics in addition to longjmp semantics in the same runtime so that
some of the main application code could happily do
"lua_pushstringorreturnerrorcode" or "lua_pushstringorraiseexception"
(which might be implemented in terms of the former), while all
remaining code can do the lua_pushstring as usual.

[3] http://lua-users.org/lists/lua-l/2006-03/msg00699.html