Monday, August 10, 2009

Secret sauce revealed

In my previous post, I showed a little program using stackless coroutines with asio. Obviously there's no yield keyword in C++, so without further ado here's the preprocessor magic that makes it possible:

class coroutine
{
public:
coroutine() : value_(0) {}
private:
friend class coroutine_ref;
int value_;
};

class coroutine_ref
{
public:
coroutine_ref(coroutine& c) : value_(c.value_) {}
coroutine_ref(coroutine* c) : value_(c->value_) {}
operator int() const { return value_; }
int operator=(int v) { return value_ = v; }
private:
int& value_;
};

#define reenter(c) \
switch (coroutine_ref _coro_value = c)

#define entry \
extern void you_forgot_to_add_the_entry_label(); \
bail_out_of_coroutine: break; \
case 0

#define yield \
if ((_coro_value = __LINE__) == 0) \
{ \
case __LINE__: ; \
(void)&you_forgot_to_add_the_entry_label; \
} \
else \
for (bool _coro_bool = false;; \
_coro_bool = !_coro_bool) \
if (_coro_bool) \
goto bail_out_of_coroutine; \
else

Unlike the preprocessor-based coroutines presented here, the above macros let you yield from a coroutine without having to return from a function. Instead, the yield simply breaks you out of the reenter block. That trick is what allows us to write a server in one function.

Yes, yes, I know. An echo server in a single function is a bit of a gimmick. The following snippet may give a better idea of how the coroutines can be used:

class session : coroutine
{
public:
session(tcp::acceptor& acceptor)
: acceptor_(acceptor),
socket_(new tcp::socket(acceptor.get_io_service())),
data_(new array<char, 1024>)
{
}

void operator()(
error_code ec = error_code(),
size_t length = 0)
{
reenter (this)
{
entry:
for (;;)
{
yield acceptor_.async_accept(
*socket_, *this);

while (!ec)
{
yield socket_->async_read_some(
buffer(*data_), *this);

if (ec) break;

yield async_write(*socket_,
buffer(*data_, length), *this);
}

socket_->close();
}
}
}

private:
tcp::acceptor& acceptor_;
shared_ptr<tcp::socket> socket_;
shared_ptr<array<char, 1024> > data_;
};

Compared to the usual boost::bind-based approach of doing callbacks, the control flow is all in one place and easy to follow.

But wait, there's more... In the next post I'll reveal the real power you get when you combine stackless coroutines and asio.

11 comments:

Marat said...

Question:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void operator()(
error_code ec = error_code(),
size_t length = 0)
{
int a = 0;

reenter (this)
{
entry:
for (;;)
{
yield acceptor_.async_accept(
*socket_, *this);

a = 1;

while (!ec)
{
yield socket_->async_read_some(
buffer(*data_), *this);

// what is a here?
a = 1;

if (ec) break;

yield async_write(*socket_,
buffer(*data_, length), *this);

// what is a here?
a = 2;

}

socket_->close();
}
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I think "a" at question places will always be 0. Am I right?

chris said...

That's correct. If you want your coroutine to have "local" variables, you should make them data members of the function object.

Daemn said...

That's interesting...

I wanted easier program flow usage, and didn't want to embed a scripting language (yet). So I wrapped it up in a macro ("CREATE_CALLBACK")..

http://pastebin.ca/1538834

Instead of deriving my class, Application::Main, from it, I pass it a an argument. Not really the same as a coroutine, but the point was the same ("the control flow is all in one place and easy to follow"). I find using Asio a lot easier this way. Any suggestions?

Thanks again for Asio!

Daemn said...

BTW I'm still debugging that code, it's nasty, but gets the point across.

chris said...

@Daemn: Looks like you would like C++0x lambdas then. However, I'm a little confused: don't your macros create local classes? What compiler are you using, because (pre-C++0x) you can't use local classes as template parameters.

Eric Muyser said...

I will like C++0x lambdas when we get to that point. Until then..

No the first argument is just the name of the local class. Second argument is the main class all the callbacks are inside, simply because 'this' pointer changes. Can't access variables outside the inner scopes, so boxing/unboxing named variable list. Really shotty, but it seems to work. It's like extremely verbose ActionScript/JavaScript function objects (usually used as callbacks).

Eric Muyser said...

@chris I ran into the local class used as a template argument error today. Good call. I -was- using Visual Studio VC9, and now I'm using MinGW.

Nicolás said...

You should explain how those macros work. There seem to be many "tricks" that are useful but aren't required to make coroutines work.

For example, it *seems* to me that the for loop inside 'yield' is just a trick to execute something *after* the yield expression while not making yield use function syntax.

Similarly for you_forgot_to_add_the_entry_label. I suspect it would be easier to follow if you remove the "feature" of: giving a readable warning if you forget the entry label.

Maybe show it as a tutorial for creating such a macro. Start simple, then add the tricks one by one...

Anonymous said...

Perhaps user error, but in VC9 this sample seems to build in Debug but not in Release. An attempt at a release build causes a link error for 'you_forgot_to_add_the_entry_label' despite the fact that the code builds and runs with a debug build.

Dave Abrahams said...
This comment has been removed by the author.
Anonymous said...

Chris - is there supposed to be another "case __LINE__:;" so you can jump back after the yield?

You have that case statement only if __LINE__ is 0 ie.,((_coro_value = __LINE__)==0).

How will it jump back to the line after the yield next time?