TL;DR
coco is a simple stackless, single-threaded, and header-only C++11 coroutine library.
https://github.com/kingluo/coco
Background
Don’t communicate through shared memory, share memory through communication.
I have always been impressed by Golang’s CSP programming, which is very expressive and couples different business logic together through channels.
In my C++ programming career, I have always struggled with callback hell. Usually, more than 5 callbacks are enough to make me confused even if I am the author, not to mention parallel asynchronous calls. On modern Linux, we use epoll or io_uring for asynchronous programming, but callback handling will make the business logic fragmented. C++20 introduced coroutines, but it is very complicated and far from the simple building blocks in Go that I need. I searched on GitHub, but did not find a corotuine library that is simple enough for my needs.
In Rust, a coroutine is implemented by an implicit state machine generated by the compiler. It is essentially a switch statement. So why not use macros to make coroutines look like Go, but use a state machine behind the scenes? Also, I don’t need multithreaded coroutines (goroutines) because in C++, usually we handle threads ourselves to improve efficiency, and we also don’t need a complex runtime like Go to schedule coroutines, so I hope the coroutine behaves like Lua, that is, it is cooperative and managed by the programmer to make it as flexible as possible.
So, I decided to build one myself (funny enough, I did something similar a decade ago, bringing Java’s reflection, annotations, and object proxies to C++). This is coco, a header file with only 200 code lines.
Design
- No depends on C++20 coroutine, C++11 is enough.
- No compiler dependencies, simple macros, header-only.
- coroutine like Lua, stackless async/await implementation just like Rust.
- channel and waitgroup like Go.
- single-threaded, no locks.
- No performance overhead.
Synopsis
|
|
Example: A simple webserver based on io_uring
In “Lord of the io_uring”, there is an example that implements a simple webserver. You can see that the callback style makes the code difficult to read and maintain.
|
|
Connection acceptance and request handling are all mixed in a big switch. Compared with golang, this is terrible, isn’t it? When you face the real complex business logic, it’s hard to imagine what the code looks like.
With coco, it can be expressed as:
|
|
Looks much better, right?
And the io_uring callback does only one thing: resumes the coroutine.
|
|
It lets you program in C++ like Go, but without any performance sacrifices.
Coding restrictions
Since Coco is simple and has no compiler support, it inevitably has some coding limitations.
- coroutine lambda functions should be reentrant, so the statements should be reentrant and produce the same flow after reentry.
- yield, wait, or channel r/w should be placed in an async block at the first level.
- variables declared cannot escape yield, so if you need some variables to survive from a yield, use the state or global variables.
- exceptions must be caught inside the lambda function.
- state subclasses must inherit the
state_t
base class anddynamic_cast
it to the subclass when using it. - The state is managed by the coroutine, so the state will be deleted when the coroutine exits.
- You must delete all created coroutines, channels, and waitgroups yourself.
Conclusion
Coco is a poor man’s C++11 coroutine library that can improve your productivity when doing asynchronous programming.
If you like it, please star my GitHub repo!