https://github.com/dankmeme01/arc
Modern C++ async runtime, inspired by Tokio
https://github.com/dankmeme01/arc
async asynchronous cpp runtime
Last synced: about 2 months ago
JSON representation
Modern C++ async runtime, inspired by Tokio
- Host: GitHub
- URL: https://github.com/dankmeme01/arc
- Owner: dankmeme01
- Created: 2025-11-15T16:05:01.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-02-05T01:49:06.000Z (about 2 months ago)
- Last Synced: 2026-02-05T01:54:07.866Z (about 2 months ago)
- Topics: async, asynchronous, cpp, runtime
- Language: C++
- Homepage:
- Size: 435 KB
- Stars: 9
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Arc
Arc is a modern C++ async runtime heavily inspired by Tokio and Rust async in general. If you've programmed async rust before - this library will be very familiar to you.
This project is WIP, some things may have bugs and not be production ready, but it is actively maintained and some things are covered by tests. Features (scroll below for examples):
* Runtime that can run using either one or multiple threads
* Tasks as an independent unit of execution
* Blocking tasks on a thread pool
* Synchronization (Mutexes, semaphores, notify, MPSC channels)
* Networking (UDP sockets, TCP sockets and listeners)
* Time utilities (sleep, interval, timeout)
* Multi-future pollers like `arc::select` and `arc::joinAll`
* Signal catching (i.e. listening for Ctrl+C easily)
* Top-level exception handler that prints the backtrace of futures, to aid in debugging
TODO:
* File IO using blocking thread pool
* Better poller. Current implementation uses `poll`/`WSAPoll`, which isn't scalable. This is not an issue for small jobs, but makes the library unsuitable for servers that handle hundreds of connections. Currently this is a non-goal as this library was mostly made for a network client rather than a server
## Getting Started
Arc supports Clang and MSVC and requires C++23. If you are using CMake with CPM, the easiest way to use Arc is as follows:
```cmake
CPMAddPackage("gh:dankmeme01/arc@v1.1.0")
target_link_libraries(mylib PRIVATE arc)
```
By default, this includes all features (networking, time, signals, etc.) except for debugging ones. For fine-grained feature control, disable `ARC_FEATURE_FULL`:
```cmake
# The line below will disable less essential (but not all) parts of Arc
set(ARC_FEATURE_FULL OFF CACHE BOOL "" FORCE)
# Manual feature selection
set(ARC_FEATURE_TIME OFF CACHE BOOL "" FORCE)
set(ARC_FEATURE_NET ON CACHE BOOL "" FORCE)
set(ARC_FEATURE_SIGNAL OFF CACHE BOOL "" FORCE)
set(ARC_FEATURE_DEBUG ON CACHE BOOL "" FORCE)
set(ARC_FEATURE_TRACE OFF CACHE BOOL "" FORCE)
```
To run any async code, you must have a runtime. Arc runtimes do not need to be unique or persistent, there is no global singleton runtime and you are responsible for creating one yourself. If you are a library developer and want to use Arc, you can spin up a runtime and run code like this:
```cpp
#include
arc::Future myFuture() {
fmt::println("Hello from async!");
co_return 42;
}
auto rt = arc::Runtime::create(4); // use 4 threads, omit to use the CPU thread count
// this will wait for `myFuture` to finish and return its result
int value = rt->blockOn(myFuture());
// this will spawn the future independently and not block
// note that the future will be aborted if the runtime is destroyed
auto handle = rt->spawn(myFuture());
```
If you are an application developer, you can use a helper macro to automatically make a runtime for you:
```cpp
#include
arc::Future<> asyncMain(int argc, const char** argv) {
fmt::println("Hello from async!");
co_return;
}
ARC_DEFINE_MAIN(asyncMain);
// alternatively, if you want to specify thread count
ARC_DEFINE_MAIN_NT(asyncMain, 4);
```
When using the helper macro, the arguments of the main function must be either `()`, `(int, char**)` or `(int, const char**)`. The return value can be:
* `int` (aka `arc::Future`) or any `T` that is convertible to `int` - value will be used as the exit code
* `void` (aka `arc::Future<>` or `arc::Future`) - exit code will be 0
* `geode::Result` where `E` can be formatted with `fmt` - returning an `Err` will print the error and exit with code 1
## Examples
All examples here may not be fully complete and are simplified for demonstration purposes. The `examples` subfolder contains examples of actual compilable programs. Some additional documentation is available [here](./docs/index.md)
Creating a runtime and blocking on a single async function, creating tasks
```cpp
#include
using namespace asp::time;
arc::Future noop() {
// `yield` temporarily yields control to the scheduler, like a very short sleep
co_await arc::yield();
co_return 1;
}
arc::Future<> asyncMain() {
// `spawn` can be used to spawn a task and let it run in the background,
// the coroutine will be running in parallel and not block the current task
auto handle = arc::spawn(noop());
// Async sleep, does not block the thread and yields to the runtime instead.
co_await arc::sleepFor(Duration::fromSecs(1));
// One second later, let's retrieve the value returned by the spawned task
int value = co_await handle;
fmt::println("{}", value);
}
ARC_DEFINE_MAIN(asyncMain);
```
Running a task every X seconds (interval)
```cpp
// create an interval that ticks every 250 milliseconds
auto interval = arc::interval(Duration::fromMillis(250));
while (true) {
co_await interval.tick();
fmt::println("tick!");
}
```
Sending data between tasks or between sync code
```cpp
arc::Task<> consumer(arc::mpsc::Receiver rx) {
while (true) {
auto val = co_await rx.recv();
if (!val.isOk()) {
break; // channel is closed now, all senders have been destroyed
}
fmt::println("received value: {}", val.unwrap());
}
}
arc::Task<> asyncMain() {
// Create a new MPSC channel with unlimited capacity
auto [tx, rx] = arc::mpsc::channel();
// Spawn a consumer task
arc::spawn(consumer(std::move(rx)));
// Sender can be copied, unlike the Receiver
auto tx2 = tx;
// Send can only fail if the channel has been closed (meaning the receiver no longer exists)
(void) co_await tx.send(1);
// `trySend` can be used in sync or async code, will fail if the channel is closed or full
auto res = tx2.trySend(2);
}
```
Synchronization utilities such as Mutex, Notify, Semaphore
```cpp
arc::Future<> asyncMain() {
arc::Mutex mtx{0};
arc::Notify notify;
// spawn a task that will wait for a notification
// if you're confused about `this auto self`, scroll to the bottom of README
auto handle = arc::spawn([&](this auto self) -> arc::Future {
co_await notify.notified();
// try to lock the mutex, this will take some time because main function waits before unlocking
auto lock = co_await mtx.lock();
co_return *lock;
}());
{
// lock the mutex and change the value
fmt::println("Locking mutex in main");
auto lock = co_await mtx.lock();
*lock = 42;
// notify the other task and wait a bit before unlocking
fmt::println("Notifying task");
notify.notifyOne();
co_await arc::sleep(Duration::fromSecs(1));
fmt::println("Unlocking mutex in main");
}
int value = co_await handle;
fmt::println("{}", value);
}
```
Creating TCP and UDP sockets
```cpp
// TcpStream is very similar to rust's TcpStream
auto res = co_await arc::TcpStream::connect("127.0.0.1:8000");
auto socket = std::move(res).unwrap();
// In the real world, check that the functions actually succeed instead of casting to void/unwrapping
char[] data = "hello world";
(void) co_await socket.send(data, sizeof(data));
char buf[512];
size_t n = (co_await socket.receive(buf, 512)).unwrap();
// UdpSocket
auto res = co_await arc::UdpSocket::bindAny();
auto socket = std::move(res).unwrap();
auto dest = qsox::SocketAddress::parse("127.0.0.1:1234").unwrap();
char[] data = "hello world";
(void) co_await socket.sendTo(data, sizeof(data), dest);
char buf[512];
size_t n = (co_await socket.receive(buf, 512)).unwrap();
```
Creating a TCP listener
```cpp
auto res = co_await arc::TcpListener::bind(
qsox::SocketAddress::parse("0.0.0.0:4242").unwrap()
);
auto listener = std::move(res).unwrap();
while (true) {
auto res = co_await listener.accept();
auto [stream, addr] = std::move(res).unwrap();
fmt::println("Accepted connection from {}", addr.toString());
arc::spawn([](arc::TcpStream s, qsox::SocketAddress a) mutable -> arc::Future<> {
// do things with the socket ...
}(std::move(stream), addr));
}
```
Putting a time limit on a future, and cancelling it if it doesn't complete in time.
```cpp
auto [tx, rx] = arc::mpsc::channel();
// Wait until we either get a value, or don't get any values in 5 seconds.
auto res = co_await arc::timeout(
Duration::fromSecs(5),
rx.recv()
);
if (res.isErr()) {
fmt::println("Timed out!");
co_return;
}
auto result = std::move(res).unwrap();
if (result.isOk()) {
fmt::println("Value: {}", result.unwrap());
} else {
fmt::println("Channel closed!");
}
```
Run multiple futures concurrently (as part of one task), wait for one of them to complete and cancel the losers. This is very similar to the `tokio::select!` macro in Rust and can be incredibly useful.
```cpp
arc::Mutex mtx;
// arc::select takes an unlimited list of selectees.
// Whenever the first one of them completes, its callback is invoked (if any),
// and the rest are immediately cancelled.
co_await arc::select(
// A future that simply finishes in 5 seconds
// (basically ensuring the select won't last longer than that)
arc::selectee(
arc::sleep(Duration::fromSecs(5)),
[] { fmt::println("Time elapsed!"); }
),
// A future that never completes, just for showcase purposes
arc::selectee(arc::never()),
// A future that will complete once we are able to
// acquire the lock on the mutex
arc::selectee(
mtx.lock(),
[](auto guard) { fmt::println("Value: {}", *guard); },
// Passing `false` as the 3rd argument to `selectee` will
// disable this branch from being polled.
false
),
// A future that waits for an interrupt (Ctrl+C) signal to be sent
arc::selectee(
arc::ctrl_c(),
// Callbacks can be synchronous, but they also can be futures
[] -> arc::Future<> {
fmt::println("Ctrl+C received, exiting!");
co_return;
}
)
);
```