Added C++20 Coroutine Support to RESTinCurl

Published

By Jarle Aase

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:

auto r = co_await client.Build()                   ->Get("https://example.com/api")                   .Option(CURLOPT_FOLLOWLOCATION, 1L)                   .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:

auto sendPush = [&](std::string token, std::string payload) -> unifex::task<bool> {    const auto url = format("https://api.push.apple.com/3/device/{}", token);    const auto r = co_await client.Build()                  ->Post(url)                  .Option(CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0)                  .WithHeader("apns-topic: com.example.app")                  .WithBody(payload)                  .CoExecute();    if (r.http_response_code != 200) {        std::cerr << "Push failed: " << r.body << "\n";        co_return false;    }    co_return true;};

...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 enable RESTINCURL_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 from await_resume().
#define RESTINCURL_ENABLE_ASYNC 1#include "restincurl/restincurl.h"unifex::task<void> myTask(Client& client) {    // co_await works for both HTTP/1.1 and HTTP/2 requests:    restincurl::Result r = co_await client.Build()                               ->Post("https://api.example.com/data")                               .Option(CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0)                               .WithBody("{ \"foo\": 42 }")                               .CoExecute();    if (r.http_response_code == 200) {        std::cout << "Success: " << r.body << "\n";    }    co_return;}

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:

#define RESTINCURL_ENABLE_ASIO 1#define RESTINCURL_ENABLE_ASYNC 1#include <boost/asio.hpp>#include "restincurl/restincurl.h"boost::asio::io_context ctx;auto f = boost::asio::co_spawn(ctx, [&]() -> boost::asio::awaitable<void> {    auto r = co_await client.Build()                  ->Get("https://example.com/status")                  .AsioAsyncExecute(boost::asio::use_awaitable);    std::cout << "Got status " << r.http_response_code << "\n";    co_return;}, boost::asio::use_future);ctx.run();f.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!