Write clean sequential code — run it on callback-based synchronous and asynchronous systems.
ca2co::continuation enables porting classic blocking code (UI, networking, filesystem, protocols) into event-driven architectures without rewriting logic into callbacks:
// ✔ Sync blocking API.
std::string blocking_api();
// ✔ Async Callback API
void async_callback_api(std::function<void(std::string)> const& callback) noexcept;
// ✔ Continuation coroutine wrapper
ca2co::continuation<std::string> any_api(bool use_blocking_api)
{
if (use_blocking_api) {
co_return blocking_api();
} else {
co_return co_await ca2co::async<std::string>(async_callback_api);
}
}
ca2co::spawn([]->ca2co::continuation<>{
auto answer = co_await co_op();
process( answer); // Is executed in the context of the underlying scheduler. Here provided be the Gui event loop.
});- ✅ Keep linear control flow (
if,for, exceptions) - ✅ Decouple business logic from UI/network async APIs
- ✅ Works with any callback-based API — no specific framework required
- ✅ No thread switching — resumes where the callback runs
Our product originated as a classic Windows application. In that environment, user interface logic often used modal dialogs, making code naturally sequential:
if (show_message_box_win("Continue?", {"Yes", "No"}) != "Yes")
return;
// continue(!) with further processingIn more modern programming models, such as QML, this pattern becomes callback-based:
show_message_box_qml("Continue?", {"Yes", "No"}, [&](std::string_view choice) {
// continue(!) with further processing
});
} // usually the end of the functionHowever, rewriting existing business logic into callback pyramids is painful — especially when the UI layer must change but the logic must stay the same.
This is where continuations come in.
With ca2co::continuation<T>, you can keep the sequential structure:
if (co_await co_show_message_box("Continue?", {"Yes", "No"}) != "Yes")
co_return;
// continue(!) with further processingca2co::continuation<std::string>
co_show_message_box(std::string const& prompt,
std::initializer_list<std::string> const& choices)
{
co_return show_message_box_win(prompt, choices);
}(using std::bind ✅)
ca2co::continuation<std::string>
co_show_message_box(std::string const& prompt,
std::initializer_list<std::string> const& choices)
{
using namespace std::placeholders;
co_return co_await ca2co::callback_async<std::string>(
std::bind(show_message_box_qml, prompt, choices, _1)
);
}Here, _1 represents the callback that callback_async uses to resume the coroutine with the result.
ca2co::continuation<T> provides:
- ✅ Suspend & resume anywhere including on the UI thread
- ✅ Zero callback nesting → linear readable control flow
- ✅ One coroutine API supports both modal and async UI
- ✅ GUI event loop compatible (Qt/QML/etc.)
- ✅ Ideal migration path for large legacy codebases
You change the implementation, not the caller.
| Library | Primary Focus / Design Goal | Special Strength | Lazy vs Eager Execution | I/O / Executor Integration | Multi-Thread / Parallelism | Interop Difficulty w/ Legacy Callbacks |
|---|---|---|---|---|---|---|
| ca2co | Turn callback-based (a)sync APIs into co_await.✅ Scheduler agnostic! |
✅ Callback → co_await bridge |
eager | depends on wrapped API | depends on wrapped API | very easy; ✅ core purpose |
| Boost.Cobalt | Coroutine-enabled async I/O with Boost.Asio | High-level async I/O primitives | mostly eager | excellent Asio integration | controlled / single-thread exec | no example found |
| cppcoro | Generic coroutine primitives & algorithms | Flexible coroutine building blocks | mostly lazy | generic & plug-in friendly | moderate | no example found |
| libcoro | Multi-threaded async runtime with schedulers + I/O | Large-scale parallel coroutine runtime | depends on awaitable | built-in I/O & schedulers | strong thread-pool parallelism | no example found |
ca2co::continuation is not limited to UI workflows.
It can be used to modernize any blocking API — such as networking, filesystem, or communication protocols.
A typical migration:
- 1️⃣ Wrap existing synchronous APIs using
ca2co::continuation - 2️⃣ Update business logic to use
co_await - 3️⃣ Replace the underlying implementation with async callbacks
Note: For a rewrite of a async asio callback example into ca2co coroutines look at the asio_ca2co to compare the callback and the ca2co coroutine variants of the echo_client.
std::string send_request_sync(std::string const& request)
{
socket.write(request);
return socket.read(); // blocking
}ca2co::continuation<std::string>
co_send_request(std::string const& request)
{
co_return send_request_sync(request);
}ca2co::continuation<void> protocol_flow()
{
auto hello = co_await co_send_request("HELLO");
if (hello != "OK")
co_return;
auto data = co_await co_send_request("GET DATA");
process(data);
co_return;
}void send_request_async(
std::string request,
std::function<void(std::string)> callback);Updated wrapper:
ca2co::continuation<std::string>
co_send_request(std::string const& request)
{
using namespace std::placeholders;
co_return co_await ca2co::callback_async<std::string>(
std::bind(send_request_async, request, _1)
);
}- ✅ Business logic requires no changes
- ✅ Underlying transport switches from sync → async
- ✅ The same coroutine flow now runs without blocking
ca2co::continuation<T> lets us:
- keep sequential UI-driven logic
- port to async architectures cleanly
- without rewriting business workflows into callbacks
- Include the
ca2coheader:
#include <ca2co/continuation.hpp>- Write coroutine-based UI logic:
ca2co::continuation<void> run_flow()
{
if (co_await show_message_box("Proceed?", {"Yes", "No"}) != "Yes")
co_return;
std::string name = co_await prompt_user_for_name();
process_user(name);
}- Start the coroutine from your UI environment:
run_flow(); // automatically starts and resumes on the UI thread- Exceptions thrown inside the coroutine propagate through
co_await. - UI-side cancellations (e.g. dialog closed) should resume with a special value.
- because the callback can only recieve one parameter, you must pack an error code into a struct, tuple or a std::expect
Example:
try {
co_await wait_for_user();
} catch (std::exception const& e) {
log_error(e.what());
}Cancellation behavior is entirely under the application's control.
ca2co::continuation does not switch threads on its own.
Where the coroutine resumes depends entirely on where the callback is invoked.
This means:
- If the callback fires on the UI thread → coroutine continues on the UI thread ✅
- If the callback fires on a worker thread → coroutine continues there
⚠️
This allows integration with Qt/QML, Win32, and other event-loop environments without forcing a specific threading model.
---## More Details