C++, pcall, and yield

classic Classic list List threaded Threaded
16 messages Options
Reply | Threaded
Open this post in threaded view
|

C++, pcall, and yield

Greg Falcon
The list already contains a few discussions about yielding across a C
boundary in general, and across pcall() in particular, but I have an
additional complication to add to the discussion: I am building Lua as
C++.

My searching has turned up two approaches to the
yield-across-C-boundary problem:

* Eric Jacobs' coroutines power patch, which introduces lua_call_yp()
and requires yieldable C functions to be written so that they can save
their state and resume themselves later if required, and

* Mike Pall's Coco, using swapcontext() or other OS API calls to
generate genuine coroutines in C.

However, I'm not sure which (if either) approach is correct for me.

The coroutines power patch is written for 5.0.2, so it would take a
bit of work to port to 5.1.  Worse, pcall() does not seem to be
adapted, and as I see it, adding pcall() support to the power patch is
a much bigger challenge than the 5.0.2=>5.1 port.  I have an idea on
how it might be done, but would like to hear the current state of
affairs for this patch before I reinvent the wheel.

Despite the version problems, this patch has the advantage of avoiding
any OS-specific calls.  This would seem to improve portability.

Coco is intriguing and looks fantastic for C, but for C++, problems
arise.  The big question mark is for try/catch handling.  One
coroutine may have a try/catch block active, but will swapping this
coroutine in and out adjust the exception handler appropriately?  My
question is not "will this work?", as this is easy enough to test for
any given platform, but rather "should this work?"

Coco uses different C-stack-swapping strategies based on platform and
compile-time #defines.  Of the four strategies listed, only the
Windows API clearly documents its behavior (and happily, Windows
fibers do properly handle exceptions).  But for Unix, my inability to
find a clear specification makes me fear portable C++ coroutines may
be impossible to write.

POSIX's ucontext interfaces seem completely unspecified with regard to
C++ behavior.  It's a set of C functions, so this isn't totally
surprising.  Coco can also use setjmp/longjmp to implement coroutines
by modifying the platform-specific contents of jmp_buf, but that makes
me very nervous indeed.

Using setjmp/longjmp for Lua errors in C++ is not a solution.
Try/catch, unlike setjmp/longjmp, ensures that objects are properly
destructed as the stack is unwound.  Avoiding try/catch is only a
suitable solution so long as no C++ objects are used within Lua C
functions, but that defeats the purpose of compiling Lua as C++ in the
first place.

Yeilding across a pcall() is something I will eventually need.  Here
are my questions for the list:

1) Has anybody faced the same problem as me?  (pcall/yield/C++)?
Insights on this would be greatly appreciated.

2) Is the Wiki up to date on the coroutines power patch?  Has anyone
done work on a 5.1 port, or on pcall() support?

3) Can anyone enlighten me on ucontext/swapcontext behavior in the
presence of C++ exception handling?  Is there some POSIX document that
specifies this that I have missed?  Or is swapcontext really unusable
in portable C++?

4) Are there any other practical approaches to the yield-across-C
problem that I have missed?

Thanks,
Greg F
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Greg Falcon
Somehow in my prior investigations, I had missed
http://lua-users.org/wiki/ResumableVmPatch , which solves the
pcall/yield/C++ problem quite elegantly.  With pure ANSI C, it gives
coroutines in Lua the power that they deserve. :)

The latest version on the wiki was a patch for 5.1w6.  I have taken
that and updated it to provide the same patch, updated for 5.1 final.
You can download it from the wiki or from
http://verylowsodium.com/lua5.1-rvm.patch .

Greg F
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Reuben Thomas-5
On Sun, 16 Apr 2006, Greg Falcon wrote:

> Somehow in my prior investigations, I had missed
> http://lua-users.org/wiki/ResumableVmPatch , which solves the
> pcall/yield/C++ problem quite elegantly.  With pure ANSI C, it gives
> coroutines in Lua the power that they deserve. :)

This is interesting. Are the Lua team considering it? It seems to be
well thought out.

--
http://rrt.sc3d.org/ | The old cliches are the best
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Roberto Ierusalimschy
> >Somehow in my prior investigations, I had missed
> >http://lua-users.org/wiki/ResumableVmPatch , which solves the
> >pcall/yield/C++ problem quite elegantly.  With pure ANSI C, it gives
> >coroutines in Lua the power that they deserve. :)
>
> This is interesting. Are the Lua team considering it? It seems to be
> well thought out.

It is *very* well thought out. As such, we need some time to digest
it :)

Something like Coco solves the problem in a much simpler and a little
more generic way. But, yes, it is not portable, and so we do not intend
to incorporate it to the Lua core. But we still feel uneasy to add that
complexity in the core to solve something that can be solved in a much
simpler way by external means. (Maybe time may cure our uneasiness...)

-- Roberto
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Adam D. Moss
Roberto Ierusalimschy wrote:
> It is *very* well thought out. As such, we need some time to digest
> it :)
>
> Something like Coco solves the problem in a much simpler and a little
> more generic way. But, yes, it is not portable, and so we do not intend
> to incorporate it to the Lua core. But we still feel uneasy to add that
> complexity in the core to solve something that can be solved in a much
> simpler way by external means. (Maybe time may cure our uneasiness...)

Maybe digesting both approaches will magically inspire a third
approach which is both simple and portable. :)

I think, however, that LuaJIT will always require 'real'
C-coroutine support for yielding from JIT'd code, hence an
unbreakable Coco dependence there (unless you want yielding
to be even pickier than regular Lua - I live with this but
it's a bit fiddly); I don't think the RVM approach can cut
it as-is.  I guess Mike would know for sure.

Given the choice of yield-anywhere and JIT, I'd reluctantly
have to choose JIT.  (On my system these are mutually exclusive,
but that's a rarity - some lucky Coco-loving platforms get
both :) ).

--adam
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Mark Hamburg-4
Will LuaJIT work when Lua's exception mechanism is redefined to use C++? Or
does the stack switching confuse the C++ exception mechanism?

I know this is a likely issue with Apple's Objective-C/Cocoa runtime unless
one hacks in to swap private globals appropriately. Most C++ runtimes don't
even expose that information.

Mark

Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Greg Falcon
On 4/17/06, Mark Hamburg <[hidden email]> wrote:
> Will LuaJIT work when Lua's exception mechanism is redefined to use C++? Or
> does the stack switching confuse the C++ exception mechanism?
>
> I know this is a likely issue with Apple's Objective-C/Cocoa runtime unless
> one hacks in to swap private globals appropriately. Most C++ runtimes don't
> even expose that information.
>
> Mark

This kind of concern is the only reason Coco wasn't suitable for my
needs.  As far as I could tell from man pages and Google searching,
POSIX's ucontext interface makes no claims about exception handling or
other C++ features.  So my fear was I'd use it, it would appear to
work, but then my code would be irreparably unportable to other Unix
flavors.  Coco can also implement C coroutines using inline assembly
or hand-modifying a jmp_buf structure for use with longjmp, but of
course these measures have even more glaring portability concerns.

For Windows, Coco uses the fibers interface to create coroutines,
which is documented to work with C++ exceptions, though there appear
to be some MSDN bloggers who consider this abuse.  (Doesn't seem like
abuse to me.)

I really like Lua's approach to C/C++.  The benefit of using of
try/catch for errors in C++ is easily misunderstood.  Its benefit is
not that it will catch unhandled exceptions, and its use is more than
just a "let's use this because it's there" measure.  Because try/catch
property unwinds the stack (where setjmp/longjmp doesn't), you can use
C++ objects inside your Lua C++ functions, and they will be property
destructed if an error is thrown (or, in this patch, if a yield occurs
in your resumable Lua C++ function).  That's something I'm not at all
ready to give up.  :)

Greg F
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

David Given
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Greg Falcon wrote:
[...]
> This kind of concern is the only reason Coco wasn't suitable for my
> needs.  As far as I could tell from man pages and Google searching,
> POSIX's ucontext interface makes no claims about exception handling or
> other C++ features.
[...]

I also discovered the hard way that glibc on Linux 2.4 kernels on some i386
platforms uses an implementation of pthreads that is incompatible with any
form of user stacks, including the kind that ucontext or pretty much any other
coroutine library uses... it's sufficient for your program merely to *link* to
the pthreads library to cause breakage.

Lots more information here:

http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=339827

- --
+- David Given --McQ-+ "If you're up against someone more intelligent
|  [hidden email]    | than you are, do something insane and let him think
| ([hidden email]) | himself to death." --- Pyanfar Chanur
+- www.cowlark.com --+
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.1 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iD8DBQFERBv7f9E0noFvlzgRAik/AJ48f670UqYY9exxWm6o5qQyY41/wwCfQAQ0
SjaFu+Dg9c3NC68HZV2I7yc=
=7lD5
-----END PGP SIGNATURE-----
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Adam D. Moss
David Given wrote:
> I also discovered the hard way that glibc on Linux 2.4 kernels on some i386
> platforms uses an implementation of pthreads that is incompatible with any
> form of user stacks, including the kind that ucontext or pretty much any other
> coroutine library uses... it's sufficient for your program merely to *link* to
> the pthreads library to cause breakage.

Wow, I'm glad I'm not the only one bitten by this problem.  I
was feeling like a Pariah or something.

--adam
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Reuben Thomas-5
In reply to this post by Roberto Ierusalimschy
On Mon, 17 Apr 2006, Roberto Ierusalimschy wrote:

> But we still feel uneasy to add that complexity in the core to solve
> something that can be solved in a much simpler way by external means.
> (Maybe time may cure our uneasiness...)

I think there are a few issues to unpick here:

1. Internal vs external complexity. The API changes are small.

2. Sometimes internal complexity is worth having in order to make users'
lives simpler. The new GC is a case in point, as, further back in
history, is the hash table implementation.

3. If you did implement something like this yourselves, you could
probably simplify it as it was integrated.

(On the other side you could question the goal of Lua to be 100% ANSI C,
and wonder whether having some platform-dependent code inside in order
to make it simpler is a good idea. I think the answer is still "no": if
you want to do this sort of thing properly, you should be writing Lua in
C--. It's hard to beat the utility of a pure ANSI C implementation.)

--
http://rrt.sc3d.org/
L'art des vers est de transformer en beaut├ęs les faiblesses (Aragon)
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Mike Pall-4-2
In reply to this post by Greg Falcon
Hi,

sorry for the late reply. Just catching up with messages after
vacation. Long reply follows:

Adam D. Moss wrote:
> I think, however, that LuaJIT will always require 'real'
> C-coroutine support for yielding from JIT'd code, hence an
> unbreakable Coco dependence there (unless you want yielding
> to be even pickier than regular Lua - I live with this but
> it's a bit fiddly); I don't think the RVM approach can cut
> it as-is.  I guess Mike would know for sure.

LuaJIT is pretty much chained to Coco. Even if a future Lua core
would be changed to include RVM, I'd have to rip it out and
replace it with Coco.

The reason behind this is a bit difficult to explain. Let's just
say that the Lua interpreter has only a few execution paths where
C stack unwinding/rewinding is required. These can be accounted
for with a limited amount of code. And RVM doesn't really rewind
the C stack -- it uses a sawtooth call pattern because this is
the only portable approach. This is why you need to write all of
your resumable C functions in a particular way.

OTOH LuaJIT adds a plethora of new code paths (specialization,
inlining) and directly encodes them into machine code. Manually
rewinding the C stack for all of these cases would be awfully
complicated. And it would need lots of adjunct data structures to
capture all of the complexity.

I guess RVM would seriously inhibit the evolution of LuaJIT. Many
advanced optimizations (like shadow slots on the C stack) are
almost impossible to handle.

Comparing this to Coco, yielding from C (or from LuaJIT generated
machine code) is dead simple. Just call lua_yield() and you're
done.

About Coco's portability: yes, it's not ANSI C, but it's still
very portable. It's hard to find a platform where it doesn't
work. If inline assembler or setjmp buffer patching makes you
uneasy: it works fine -- that's all that counts. Often enough
it's easier to add some OS or machine specific code than relying
on a portable solution which has too many side effects (see the
DirectX vs. lua_number2int() discussion).

Mark Hamburg wrote:
> Will LuaJIT work when Lua's exception mechanism is redefined to use C++? Or
> does the stack switching confuse the C++ exception mechanism?

AFAIK yes. LuaJIT only runs on x86 right now. The Windows
per-thread exception pointer is taken care of with Fibers. Most
x86 platforms I know of use either frame pointer unwinding (and
LuaJIT leaves the frame-pointer alone) or in the absence of it
unwind records (e.g. DWARF2 with GCC). Both should work (but see
below).

I have no good answer about the compatibility of C++ and stack
switching in general (i.e. Coco and not just LuaJIT). One has to
check the docs or just try it on each platform one needs to
support. :-/

> I know this is a likely issue with Apple's Objective-C/Cocoa runtime unless
> one hacks in to swap private globals appropriately. Most C++ runtimes don't
> even expose that information.

Since I've discussed this privately with Mark before, I should
note that the outcome was that RVM does _not_ solve the ObjC
issue and is generally incompatible with it. But there was a
relatively simple (untested) solution for Coco.

Greg Falcon wrote:
> I really like Lua's approach to C/C++.  The benefit of using of
> try/catch for errors in C++ is easily misunderstood.  Its benefit is
> not that it will catch unhandled exceptions, and its use is more than
> just a "let's use this because it's there" measure.  Because try/catch
> property unwinds the stack (where setjmp/longjmp doesn't), you can use
> C++ objects inside your Lua C++ functions, and they will be property
> destructed if an error is thrown (or, in this patch, if a yield occurs
> in your resumable Lua C++ function).  That's something I'm not at all
> ready to give up.  :)

But there's a caveat which has been missed in the previous
discussion: there is no guarantee that it's safe to unwind across
the Lua core. Even if you redefine the Lua exception mechanism to
use try/except.

The reason can be had by turning your argument around: Lua is
written in C and does not hook into the C++ exception mechanism.
So it cannot restore its state in all places it may need to.

E.g. throwing a C++ exception and catching it with pcall() may
create an invalid Lua stack slot (L->status is -1,
luaD_seterrorobj doesn't handle this case, but still increments
the stack). This may be considered a bug (RVM solves this by
defining LUA_ERREXC).

Just assuming you can throw an exception through the Lua core
without affecting its internal state is bound to break in special
circumstances or in future Lua versions.

The only safe way to handle C++ exceptions is to catch them on
the C++ side and convert them to Lua errors or appropriate return
values (nil + error string convention).

Roberto Ierusalimschy wrote:
> Something like Coco solves the problem in a much simpler and a little
> more generic way. But, yes, it is not portable, and so we do not intend
> to incorporate it to the Lua core. But we still feel uneasy to add that
> complexity in the core to solve something that can be solved in a much
> simpler way by external means. (Maybe time may cure our uneasiness...)

Considering that I wrote both Coco and RVM, this may be a
surprising opinion -- I've changed my mind several times on this
issue: IMHO neither should be added to the Lua core right now.

Both solutions have their benefits and drawbacks. This issue
cannot be completely solved while at the same time keeping your
primary goals for the Lua core (simplicity and portability).

The question is whether a partial solution is desirable and/or
acceptable (see below).

Adam D. Moss wrote:
> Maybe digesting both approaches will magically inspire a third
> approach which is both simple and portable. :)

The "yield across pcall" issue could be solved in a different way
by changing the C API (not relying on an exception handler on the
C stack for it). Basically pcall() would need to tag the current
frame as exception catching, set up the new frame and return to
the Lua core with a special return value signalling a call to the
new frame and a handler which catches any exceptions or untags
the current frame.

The "yield from C" issue can only be worked around. RVM has
lua_vyield(), but this becomes cumbersome when you need to
yield from inside several C call levels. Still much better than
not having the option.

The "yield from everywhere" issue (metamethods, iterators and so
on) may be partly solved by restructuring the Lua core (using the
pcall mechanism outlined above). But this is not a solution for C
callbacks into the Lua core (e.g. lua_gettable).

Bye,
     Mike
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Greg Falcon
On 4/18/06, Mike Pall <[hidden email]> wrote:\
> Greg Falcon wrote:
> > I really like Lua's approach to C/C++.  The benefit of using of
> > try/catch for errors in C++ is easily misunderstood.  Its benefit is
> > not that it will catch unhandled exceptions, and its use is more than
> > just a "let's use this because it's there" measure.
[...snip...]
> Just assuming you can throw an exception through the Lua core
> without affecting its internal state is bound to break in special
> circumstances or in future Lua versions.

But the benefit I'm talking about has nothing to do with throwing
exceptions THROUGH the Lua core.  Here is a snippet of C++ which
demonstrates what I think is useful about compiling lua as C++.  (I
just threw this together to get the point across, so sorry for any
syntax errors, etc)

  // Function defined elsewhere which reads from a file and may call lua_error
  void DoSomethingWith(lua_State* L, std::ifstream& file);

  int LuaDoFileThing(lua_State* L) {
    std::ifstream file;
    file.exceptions(ifstream::eofbit | ifstream::failbit | ifstream::badbit);
    try {
      file.open("test.txt");
      while (!file.eof())
        DoSomethingWith(L, file);
    }
    catch (std::ifstream::failure e) {
      lua_pushstring(L, "Error handling file");
      lua_error(L);
    }
    file.close();
    return 0;
  }

Here LuaDoFileThing is a Lua C++ function.  I see two main benefits:
1. If a file exception is thrown, either from file.open() or from
inside DoSomethingWith(file), it is converted to a Lua error in one
place.
2. If DoSomethingWith(file) calls lua_error, the stack is properly
unwound and the file object is destructed, avoiding resource leaks

Using compiled-as-C Lua binaries from C++ (using lua.hpp / extern "c"
wrappers) removes benefit #2.  The C++ standard explicitly states that
setjmp/longjmp is not guaranteed to call destructors of objects as it
forcibly unwinds the stack.

My fear of using stack-swapping tricks is different.  Does the
ucontext interface properly handle try/catch blocks?  The
documentation I've been able to find doesn't say.  I'm afraid that, at
least on some platforms, it will mess up try/catch blocks, and then
either other coroutines will incorrectly behave as though there was an
active ifstream::failure exception handler, or that this coroutine
will be incorrectly resumed with a broken or non-functional
ifstream::failure exception handler.

These concerns really depend on how important portability is as a
goal.  If I was developing a program to run on just a small handful of
platforms, then I'd test Coco's effect on try/catch handlers and would
use it if things worked.  More to the point, I'd use LuaJIT in that
case, as well.  But it depends on your goals.  Right now, for me,
portability trumps elegance.

A few code correctness notes:

IMO, Lua really ought to be fixed to use catch(struct lua_longjmp)
rather than catch(...), since lua_longjmp is the only type of object
Lua will throw.  Lua has no business catching other exceptions.
Better to let unhandled exceptions remain unhandled.

The Resumable VM patch is not quite 100% portable ANSI C yet, as it
assumes an int can be stored in a void* and retrieved as an int later.
 The C standard says casting an int to void* can generate a trap
representation (6.3.2.3 p5) which can cause undefined behavior later
when the void* ctx is read (6.2.6.1 p5).

Greg F
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Mike Pall-4-2
Hi,

Greg Falcon wrote:
> IMO, Lua really ought to be fixed to use catch(struct lua_longjmp)
> rather than catch(...), since lua_longjmp is the only type of object
> Lua will throw.  Lua has no business catching other exceptions.
> Better to let unhandled exceptions remain unhandled.

But then Lua can't restore its own state. See the (status != 0)
cases in LuaD_pcall or lua_resume. So a catch(...) may be ugly,
but at least it doesn't screw up the Lua state (with the
LUA_ERREXC bug fix of course).

BTW to Roberto: I assume the LUA_ERREXC bug fix will be part of
Lua 5.1.1-rc1?

> The Resumable VM patch is not quite 100% portable ANSI C yet, as it
> assumes an int can be stored in a void* and retrieved as an int later.
>  The C standard says casting an int to void* can generate a trap
> representation (6.3.2.3 p5) which can cause undefined behavior later
> when the void* ctx is read (6.2.6.1 p5).

Well, you could work around this by using a union instead of a
void* everywhere. But I seriously doubt there is a platform still
in common use with this behaviour. It would be quite esoteric,
anyway.

Bye,
     Mike
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Greg Falcon
On 4/18/06, Mike Pall <[hidden email]> wrote:

> Greg Falcon wrote:
> > IMO, Lua really ought to be fixed to use catch(struct lua_longjmp)
> > rather than catch(...), since lua_longjmp is the only type of object
> > Lua will throw.  Lua has no business catching other exceptions.
> > Better to let unhandled exceptions remain unhandled.
>
> But then Lua can't restore its own state. See the (status != 0)
> cases in LuaD_pcall or lua_resume. So a catch(...) may be ugly,
> but at least it doesn't screw up the Lua state (with the
> LUA_ERREXC bug fix of course).

Interesting.

I'd like to throw the following alternative out there:

#define LUAI_TRY(L,c,a) try { a } catch(lua_jumpbuf&) \
        { if ((c)->status == 0) (c)->status = -1; } \
        catch(...) { /* possibly cleanup and call panic first? */
exit(EXIT_FAILURE); }

My fear with catch(...) is the presence of an (otherwise) unhandled
exception implies the very real possibility that the host application
is now in an inconsistent state.  Code has branched in a way the
author did not anticipate for reasons Lua cannot understand; Lua can
fix its state but cannot fix the application's.

Dissapointingly, catch(...) in the MSVC compiler will catch things
like null pointer accesses and segfaults (always in older versions and
based on compiler flags in newer ones, IIRC).  Continuing on in the
face of that sort of error always worries me, so MSVC is where I
gained my deep distrust of catch(...).

The good news is that this is highly localized luaconf.h stuff.  Part
of me feels like a LUA_ABORT_ON_UNHANDLED_EXCEPTION compile-time flag
might be appropriate, but it's hard to imagine bloating luaconf.h for
such a small issue.

Greg F
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Roberto Ierusalimschy
In reply to this post by Mike Pall-4-2
> But then Lua can't restore its own state. See the (status != 0)
> cases in LuaD_pcall or lua_resume. So a catch(...) may be ugly,
> but at least it doesn't screw up the Lua state (with the
> LUA_ERREXC bug fix of course).
>
> BTW to Roberto: I assume the LUA_ERREXC bug fix will be part of
> Lua 5.1.1-rc1?

The bug is only in 'luaD_seterrorobj', that increments top without
pushing a corresponding value, is that right?

-- Roberto
Reply | Threaded
Open this post in threaded view
|

Re: C++, pcall, and yield

Mike Pall-4-2
Hi,

Roberto Ierusalimschy wrote:
> The bug is only in 'luaD_seterrorobj', that increments top without
> pushing a corresponding value, is that right?

And of course in LUAI_TRY where the status should be set to a
defined value and not -1 (e.g. I used #define LUA_ERREXC 6).

Bye,
     Mike