[Date Prev][Date Next][Thread Prev][Thread Next]
[Date Index]
[Thread Index]
- Subject: Re: pcall() and coroutine.yield()
- From: Mike Pall <mikelu-0410@...>
- Date: Tue, 19 Oct 2004 04:18:59 +0200
Hi,
Michael Roth wrote:
> Lua 5.0.2. can't yield accross a pcall().
> [...]
> Ok, at least this is the way currently lua is implemented, because
> pcall() use setjmp()/longjmp() and coroutines aren't implemented using
> setjmp()/rightjmp(), right?
Yes, this is the sad state of affairs. I have been bitten by this and it
forced me to restructure my code in awkward ways. Being able to mix pcall()
and coroutines would *really* help.
The coroutine-on-top-of-coroutine solution demonstrated by Roberto may be
ok for some cases. But it is definitely not feasible for my code due to
the high overhead of creating and destroying millions of ultra-short
running coroutines per second. This is not just a lightweight function call
after all. :-(
> If lua 5.1 doesn't help, would it be possible to change lua in that way,
> that you can push an error handler function onto the stack followed by
> the function to call in a protected environment together with its
> arguments? Something like this:
>
>
> lua_pushcfunction(L, err_handler);
> lua_push_some_function(L, ...);
> lua_push_some_arguments(L, ...);
>
> return lua_trycall(L, nargs); /* like lua_yield() */
I have thought about a similar solution, too. But there are really three
different issues here:
1. The C stack frame that gets added when you execute lua_pcall() from C code.
2. Where to store the setjmp() jmp_buf.
3. Which C frame to return to with longjmp().
The first problem can be solved with your idea. It is sort of a special
tail-call from C. In fact I would implement a generic C tail-call facility:
push function; push args; return lua_tailcall(L, nargs);
The latter just returns -2 (like lua_yield(), which returns -1) to signal
the C tail-call to the Lua core. Then pcall() could be easily implemented
on top of this feature.
But I think a "side-call" would be even more useful (tell me, if there
is a better name for it). This allows one to call another Lua function
from C, but return and continue with C code and not the surrounding
(Lua) code. Without adding a C stack frame, of course.
The continuation function could be an extra C function or (in most cases)
the same function -- if it gets to keep the state of the stack that was
below the arguments to the side-call feature. This is sort of like calling
yourself again, just with modified arguments.
The latter would be extremely handy for callbacks from C code and
would also help with coroutine based threading. For now I have to
wrap each C functions which may block on I/O with a Lua loop, like this:
function dispatcher_receive(sk, pattern)
local s, err, part
repeat
s, err, part = sk:receive(pattern, part)
if s or err ~= "timeout" then return s, err end
-- note: timeout is zero, so this is really a "blocking" indication
coroutine.yield(whatever_you_need_to_send_to_your_dispatcher)
until false
end
Ok, the yield can be put inside the C call, with some trickery inside the
dispatcher. But there is currently no way to avoid the Lua loop around it.
And this is not just slow and annoying -- you get into deep, deep trouble
with partial reads/writes unless the API has been designed just right
(Diego changed LuaSocket after I asked for it -- thank you again!).
There is a subtle interaction with the "part" variable in the code above.
Unfortunately this model is not generic enough for all I/O calls.
Of course the C function *itself* knows best how to resume itself. A Lua
wrapper may need to break layering, too. Oh and before I forget: blocking
on I/O is the norm and not the exception, so this needs to be fast.
With a side-call feature, the C code could yield and then return back
to itself. An obvious improvement is to merge side-calls and lua_yield()
into a single function that takes different flags and allows different
behaviours: yield, yield+cont, side-call+cont or tail-call.
[Another idea would be to allow pushing/popping of frames to/from the
Lua stack from the C API ... but I haven't thought through this in detail.
This would be a meta-mechanism in true Lua style and allows implementing
all kinds of interesting functions outside of the Lua core.]
For the second and third problem above, I may have an interesting solution.
I have not tried to code this, since it requires quite a bit of restructuring
of some Lua core parts:
- Call setjmp() in a surrounding function *before* entering the core
VM loop. Do this only once and *do not* repeat it for every pcall().
Store the jmp_buf in per-coroutine state (only one jmp_buf per state).
- pcall() uses the C tail-call or C side-call feature to avoid building up
an extra C frame. It also saves the call frame position by setting
a special pcall flag in the call frame (to stop the unwinding later on).
[This could be just another flag for the side-call function.]
- error() does a longjmp() back to the function that surrounds the VM loop.
There the Lua stack is unwound until the top pcall flag is found. The
error status is pushed and the routine starts from the top (i.e. does
a setjmp() and calls the VM loop).
With the side-call feature, a pcall_cont() routine could be called after
the unwind to do the return stack manipulation there. This allows you
to write special try/catch schemes yourself, too (e.g. unwind until
a matching catch is found, faking return values, ...).
[This is just a rough sketch of the required changes -- please ask if
I didn't make myself clear enough.]
As an added benefit, this would make pcall() faster (setjmp() may be
expensive on some platforms). And error() (though usually not relevant for
performance) would at least not be slower.
So what does everyone think about this solution? I think all of this could
work well and would help solve many related problems.
I'm really interested to have a (good/elegant) solution to the pcall + yield
problem (and my non-blocking-I/O + yield problem, too) in the Lua core.
BTW: An 'officially supported' way to recycle coroutines would be nice, too.
Bye,
Mike