Skip to content
qb
Menu
GitHub

C++20 · optional C++23 · Apache-2.0

Actors that share nothing. I/O that never blocks.

qb is a C++20-first framework for concurrent and distributed systems on the actor model. An actor is thread-affine to one core, processes one event at a time, and talks only by message — so data races and deadlocks stop being possible rather than being avoided.

greeter.cpp
#include <qb/main.h>
#include <qb/actor.h>
#include <qb/io.h>

struct GreetingEvent : qb::Event {
    qb::string<64> message;
    explicit GreetingEvent(const char *msg) : message(msg) {}
};

class GreeterActor : public qb::Actor {
public:
    qb::io::async::task<bool> onInit() final {
        registerEvent<GreetingEvent>(*this);  // subscribe
        push<GreetingEvent>(id(), "Hello");  // send to self
        co_return true;  // actor is ready
    }

    void on(const GreetingEvent &event) {
        qb::io::cout() << "Received: " << event.message << '\n';
        kill();  // work done; terminate this actor
    }
};

int main() {
    qb::Main engine;
    engine.addActor<GreeterActor>(0);  // run on core 0
    engine.start();  // start the engine
    engine.join();  // block until all actors stop
    return 0;
}
qb-core
Actor engine — scheduling, mailboxes, lifecycle
qb-io
Async runtime — libev loop, transports, coroutines
Standalone
qb-io is usable without the actor engine
3 platforms
Linux · macOS · Windows, on x86-64 and ARM64

The contract

Five rules decide whether qb code is correct.

Everything else is detail. They come from the framework's own code-generation guidance, and they are the fastest way to understand what qb guarantees and what it asks of you in return.

  1. Time is std::chrono

    Every timeout, TTL, interval and delay is a qb::duration — std::chrono::nanoseconds. It accepts finer-or-equal chrono literals and rejects bare integers at compile time. Subtracting a steady clock from a system clock does not compile.

    qb::duration
  2. Actors share nothing

    An actor is thread-affine to one VirtualCore, processes one event at a time, and communicates only by events. No mutexes, no shared mutable state, and no actor is ever constructed outside a worker thread.

    qb::VirtualCore
  3. Sending is noexcept

    push, send and broadcast are noexcept, so a throwing event constructor or an allocation failure calls std::terminate. Events are relocated with memcpy: no member may point into its own storage, which is why a by-value std::string is never a valid event member.

    push / send / broadcast
  4. A coroutine captures by value

    The actor can be destroyed while the coroutine is suspended. After any suspension the only legal channel back is the context. Prefer Actor::spawn, bound to the actor's cancellation scope, over spawn_detached.

    Actor::spawn
  5. Register every event you handle

    Call registerEvent for each event type inside onInit(), which is itself a coroutine returning a task. Returning false or throwing fails creation and yields an invalid ActorId.

    registerEvent<T>(*this)

The engine

One worker thread per core, one mailbox each.

qb::Main launches a VirtualCore per engine core. Each worker owns its actors and drains a private lock-free mailbox. The only genuinely multi-threaded surface in the whole system is the message-passing layer between cores — which is why placement is a decision you make, not one the runtime hides.

Event delivery across two VirtualCoresTwo worker threads. On the left, core 0 holds actors A, B and C; on the right, core 1 holds D and E. An event from A to B stays inside core 0's local pipe. An event from C to E crosses into core 1's multi-producer single-consumer mailbox, which only core 1 drains.VirtualCore 0VirtualCore 1ABCDElocal pipemailboxMPSCmailboxMPSCcross-core — lock-free MPSC ring
Same-core delivery never leaves the thread. Only cross-core delivery touches the ring.

Same core

The event is appended to the local pipe and consumed in the same loop iteration, after the current handler returns. No mailbox, no inter-thread synchronisation.

Across cores

The event is published into the destination core's MPSC ring and dequeued on its next iteration. This is the only path that touches lock-free machinery.

Idle latency is a knob

A mailbox latency of 0 busy-spins for the lowest possible latency and occupies a core. Above 0, the consumer parks on a condition variable and a producer wakes it — less CPU at idle, up to one span of wake-up delay.

Messaging

Five primitives, five different contracts.

Actors communicate only by typed events. The primitives differ in ordering, in whether they allocate a new event or redirect the one you received, and in what the runtime may do under backpressure. When unsure, the answer is push.

Five primitives, five different contracts.
PrimitiveOrderingEvent objectNotes
pushFIFO per source→destNewThe default. Constructed at the back of the pipe; returns a mutable reference so the sender can finish populating it.
sendNoneNewConstructed at the front. Requires a trivially destructible event; the engine may drop EventQOS0 under backpressure.
replyRedirects one eventReusedSends the received event back to its source. The handler must take a non-const reference.
forwardRedirects one eventReusedRedirects the received event to a new destination, preserving the original source.
broadcastNoneNewFans out to every actor on every core via send, so it inherits send's lack of ordering.

In practice

A server, a client, a coroutine.

Network endpoints are actors with a mixin: I/O runs on the actor's own event loop and parsed messages arrive as ordinary handler calls. Nothing is registered globally, and nothing blocks a core.

qb-io

The runtime works without the actors.

qb-io is a single-threaded, libev-backed event loop with non-blocking transports, a message-framing protocol layer and a native C++20 coroutine layer. qb-core is built on it, and qb-io can be taken alone. Application protocols like HTTP live in modules, not here.

Transports

  • TCP
  • UDP
  • SSL/TLS
  • QUIC

Protocols

  • text
  • binary8/16/32
  • JSON
  • MessagePack
  • AProtocol

Coroutines

  • task<T>
  • channels
  • generators
  • when_all
  • retry

Utilities

  • crypto & JWT
  • compression
  • URI
  • UUID
  • lock-free queues

Standalone

The runtime on its own

Every sample here is a plain executable that drives its own loop: no actor, no engine. That is the claim the section makes, so it is the one worth showing.

Modules

The protocols ship as modules, not as core.

Each is its own repository, added as a submodule and discovered by CMake. They are compiled libraries built on qb-io, so they run on the same event loop as your actors and never block a core.

qbm-http

Stable

Verified against qb 3.0.0

HTTP servers and clients as actors: a fluent router with path parameters, groups and controllers, a middleware pipeline, request validation, cookies and multipart, and both callback and coroutine client APIs.

Provides

  • HTTP/1.1
  • HTTP/2
  • HTTP/3
  • WebSocket
  • routing
  • middleware
  • validation
  • JWT

HTTP/1.1, routing, middleware and validation are always built. HTTPS, HTTP/2, WebSocket and JWT need an SSL build; HTTP/3 additionally needs QUIC and libnghttp3. The gates are real #ifdef boundaries, so what your code can reach is what was compiled.

Umbrella header
<qbm/http/http.h>
View repository

qbm-pgsql

Stable

Verified against qb 3.0.0

The PostgreSQL v3 wire protocol spoken directly over a qb-io socket. The handshake, authentication, the simple and extended query protocols, type encoding and LISTEN/NOTIFY are all implemented on the event loop.

Provides

  • no libpq
  • SCRAM-SHA-256
  • prepared statements
  • transactions
  • savepoints
  • LISTEN/NOTIFY
  • typed results

timestamptz round-trips through qb::wall_time at integer-microsecond precision. TLS is available on an SSL build. The client does not require actors — a plain executable that drives the loop works.

Umbrella header
<qbm/pgsql/pgsql.h>
View repository

qbm-redis

Stable

Verified against qb 3.0.0

A native RESP2 and RESP3 parser over a qb-io socket. Every command group is a mixin on the client: strings, hashes, sets, sorted sets, streams, scripting, ACL and cluster.

Provides

  • no hiredis
  • RESP2 / RESP3
  • full command surface
  • pipelining
  • pub/sub
  • streams
  • scripting
  • cluster

Auto-reconnect uses exponential backoff, and a command deadline drops a connection whose reply never arrives. Like qbm-pgsql, it runs without actors if you drive the loop yourself.

Umbrella header
<qbm/redis/redis.h>
View repository

In practice

What each one actually looks like

Every sample below is taken from the module’s own documentation, which is generated against its headers, so what you read here is what compiles.

qbm-http

One router, four transports. A handler written for HTTP/1.1 serves HTTP/2 and HTTP/3 unchanged, because every server feeds the same Router.

Choosing a client

The three persistent clients share the connect and push_request shape. These are the rows that decide which one you want.

Choosing a client
Capabilityhttp1http2http3
Compile gatenoneQB_HAS_SSLQBM_HTTP_HAS_HTTP3
Transport (ALPN)TCP / TLSTLS only (h2)QUIC (h3)
Base URI schemehttp:// or https://https:// onlyhttps:// only
Concurrencyone active, rest queuedmultiplexedmultiplexed

qbm-pgsql

The wire protocol, not a wrapper around libpq. Every call has a coroutine form and a callback form under the same method name.

qbm-redis

A native RESP parser and the full command surface, each group a mixin on the client. Redis errors arrive as a false reply, never as an exception.

Adding one to a project

Two steps. The CMake helper discovers every module under the directory you point it at, so the second step is the same whichever module you added.

Add the repository
git submodule add https://github.com/isndev/qbm-http qbm/http
Discover and link
qb_load_modules("${CMAKE_CURRENT_SOURCE_DIR}/qbm")
target_link_libraries(my_app PRIVATE qbm::http)

Writing your own

A qbm module is a contract rather than a private arrangement: the scaffold that produced these three is a script you can run. It generates the target, the qbm:: namespace, a tiered test suite and a superbuild root that can run those tests.

Scaffold a module
curl -fsSL https://raw.githubusercontent.com/isndev/qb/main/script/qb-new-module.sh \
  | bash /dev/stdin mymodule

Install

CMake 3.24, and dependencies fetch themselves.

A generator writes a project already wired to qb. Or clone the framework and run its test suite.

Scaffold a project
curl -fsSL https://raw.githubusercontent.com/isndev/qb/main/script/qb-new-project.sh \
  | bash /dev/stdin MyProject
cd MyProject
Configure and build
cmake -DCMAKE_BUILD_TYPE=Release -B build
cmake --build build --parallel
Build from source
git clone --recursive https://github.com/isndev/qb.git
cd qb
cmake -DCMAKE_BUILD_TYPE=Release -B build
cmake --build build --parallel
ctest --test-dir build --output-on-failure
Opt into what you deploy
cmake -B build \
  -DCMAKE_BUILD_TYPE=Release \
  -DQB_WITH_SSL=ON \
  -DQB_WITH_COMPRESSION=ON \
  -DQB_WITH_QUIC=ON \
  -DQB_BUILD_TESTS=ON

Build options

Every capability beyond the core is a flag, so a build carries only what it uses.

QB_WITH_SSL
SSL/TLS transports
QB_WITH_COMPRESSION
Payload compression
QB_WITH_QUIC
QUIC and HTTP/3
QB_WITH_LOGGING
Structured logging
QB_BUILD_TESTS
Framework test suite
QB_BUILD_BENCHMARKS
Google Benchmark suites

Platform support

  • Linux — GCC and Clang, libstdc++CI enabled
  • macOS — Apple Clang, libc++CI enabled
  • Windows — MSVCSupported, CI disabled

x86-64 and ARM64 on all three.

Contact

Talk to the maintainers.

Questions about adopting qb, a module you need, or a production deployment. Bugs and feature requests belong in GitHub issues.

Open an issue

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.