Published:

Jarle Aase

Added C++20 Coroutine Support to RESTinCurl

bookmark 2 min read

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:

Two Flavors of Awaitable Execution

1. CoExecute() – Plain C++20 Awaitable

This member returns a custom awaiter that:

 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:

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!