Added C++20 Coroutine Support to RESTinCurl
Why Coroutines Matter
As I build more components in modern C++, I find myself relying ever more on C++20 coroutines. Whenever my code needs to “wait for something” – an HTTP request, a timer, a file I/O – coroutines let me express asynchronous flows as straight-line code. But to make that work, you need libraries that expose awaitable interfaces.
Until now, RESTinCurl has provided a simple, header-only wrapper around libcurl for both synchronous and callback‐based asynchronous HTTP calls. Today I’m pleased to announce that RESTinCurl’s RequestBuilder also offers native C++20 coroutine support, so you can write:
1auto r = co_await client.Build()
2 ->Get("https://example.com/api")
3 .Option(CURLOPT_FOLLOWLOCATION, 1L)
4 .CoExecute();
... and have that work whether you’re talking HTTP/1.1 or HTTP/2, without pulling in heavyweight dependencies.
My Motivation
The backend for NextApp, my productivity app, needs push support to mobile devices. For Google and Android, I could have hacked something in Boost.Beast (if I had the time), or just used rest-cpp. Google’s API works fine with HTTP/1.1. Apple, on the other hand, requires HTTP/2 for their push notification API for iPhones.
RESTinCurl, on the other hand, a lightweight C++ wrapper over libcurl that I wrote when I implemented a cross-platform C++ library for a crypto wallet, would be perfect. The only "problem" was its async callback interface. I don’t do callbacks anymore (if I can avoid it).
So, I decided to add C++20 coroutine support. Now it’s perfect for the job. My server already uses Boost.Asio for its coroutines, so I added support for that as well as standard, generic C++20 coroutines. For some reason, Boost.Asio doesn’t support the generic coroutine interface.
For the push notifications to iPhones, I can write something like:
1auto sendPush = [&](std::string token, std::string payload) -> unifex::task<bool> {
2 const auto url = format("https://api.push.apple.com/3/device/{}", token);
3 const auto r = co_await client.Build()
4 ->Post(url)
5 .Option(CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0)
6 .WithHeader("apns-topic: com.example.app")
7 .WithBody(payload)
8 .CoExecute();
9
10 if (r.http_response_code != 200) {
11 std::cerr << "Push failed: " << r.body << "\n";
12 co_return false;
13 }
14 co_return true;
15};
...if I use the Unifex coroutine library. For Boost, the code would be almost the same (see below).
Lightweight, Header-Only, No Boost Required
RESTinCurl was designed for minimal dependencies (just libcurl) and small binary size. Adding coroutine support didn’t change that philosophy:
- Header-only: you only need to include
restincurl.h
and enableRESTINCURL_ENABLE_ASYNC
. - Single worker thread: a dedicated thread services all in-flight requests.
- No Boost. Unless you really want it: if you already use Boost.Asio, you can integrate via a second awaitable (see below), but coroutines work out of the box without Asio.
Two Flavors of Awaitable Execution
1. CoExecute()
– Plain C++20 Awaitable
This member returns a custom awaiter that:
- Suspends the coroutine.
- Enqueues the request on RESTinCurl’s worker thread.
- Resumes when the HTTP response arrives.
- Returns the completed
restincurl::Result
fromawait_resume()
.
1#define RESTINCURL_ENABLE_ASYNC 1
2#include "restincurl/restincurl.h"
3
4unifex::task<void> myTask(Client& client) {
5 // co_await works for both HTTP/1.1 and HTTP/2 requests:
6 restincurl::Result r = co_await client.Build()
7 ->Post("https://api.example.com/data")
8 .Option(CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0)
9 .WithBody("{ \"foo\": 42 }")
10 .CoExecute();
11
12 if (r.http_response_code == 200) {
13 std::cout << "Success: " << r.body << "\n";
14 }
15 co_return;
16}
No templates beyond what you’re already using, and no extra files to compile.
2. AsioAsyncExecute()
– Optional Boost.Asio Integration
If your project already uses Boost.Asio, you can seamlessly slot RESTinCurl into your io_context
. Just enable RESTINCURL_ENABLE_ASIO
and write:
1#define RESTINCURL_ENABLE_ASIO 1
2#define RESTINCURL_ENABLE_ASYNC 1
3
4#include <boost/asio.hpp>
5#include "restincurl/restincurl.h"
6
7boost::asio::io_context ctx;
8
9auto f = boost::asio::co_spawn(ctx, [&]() -> boost::asio::awaitable<void> {
10 auto r = co_await client.Build()
11 ->Get("https://example.com/status")
12 .AsioAsyncExecute(boost::asio::use_awaitable);
13
14 std::cout << "Got status " << r.http_response_code << "\n";
15 co_return;
16}, boost::asio::use_future);
17
18ctx.run();
19f.get();
AsioAsyncExecute(use_future)
integrates directly into Asio’s completion/coroutine machinery.
Note that the worker-thread used to drive libcurl is still an internal thread managed by RESTinCurl.
Conclusion
With RESTinCurl’s new coroutine APIs you get:
- A lightweight wrapper over libcurl
- Header-only, single thread per Client instance.
- Native C++20 coroutines for any async HTTP/1.1 & 2 request
- Optional seamless Boost.Asio support
If your next C++20 component needs to “wait” for anything – REST calls, timers, or I/O – having coroutine-capable libraries is essential. Give the new coroutine APIs in RESTinCurl a try today.
Happy coding!
- Link to RESTinCurl