To begin, clone the repository or download the source directlly from GitHub. The entire library is header-only — all code resides in the src/ directory, primarily within uvw.hpp. To use it, simply include this header and link against libuv. Ensure your libuv version matches the one specified in uvw’s release tags.
Basic Server and Client Example
Below is a complete example demonstrating a TCP echo server and client using uvw:
#include <uvw.hpp>
#include <cassert>
#include <iostream>
#include <memory>
#include <chrono>
void startServer(uvw::Loop& loop) {
auto server = loop.resource<uvw::TcpHandle>();
server->on<uvw::ErrorEvent>([](const uvw::ErrorEvent&, const uvw::TcpHandle&) {
std::cerr << "Server error occurred.\n";
});
server->once<uvw::ListenEvent>([server](const uvw::ListenEvent&, uvw::TcpHandle& srv) {
std::cout << "Server listening...\n";
auto client = srv.loop().resource<uvw::TcpHandle>();
client->on<uvw::ErrorEvent>([](const uvw::ErrorEvent&, const uvw::TcpHandle&) {
std::cerr << "Client error.\n";
});
client->on<uvw::CloseEvent>([server](const uvw::CloseEvent&, const uvw::TcpHandle&) {
std::cout << "Client closed. Shutting down server.\n";
server->close();
});
client->on<uvw::DataEvent>([](const uvw::DataEvent& evt, const uvw::TcpHandle&) {
std::cout.write(evt.data.get(), evt.length);
std::cout << "\nData length: " << evt.length << "\n";
});
client->on<uvw::EndEvent>([](const uvw::EndEvent&, uvw::TcpHandle& cli) {
std::cout << "Client data stream ended.\n";
cli.close();
});
srv.accept(*client);
client->read();
auto localAddr = srv.sock();
auto remoteAddr = client->peer();
std::cout << "Local: " << localAddr.ip << ":" << localAddr.port << "\n";
std::cout << "Remote: " << remoteAddr.ip << ":" << remoteAddr.port << "\n";
});
server->bind("127.0.0.1", 8080);
server->listen();
}
void startClient(uvw::Loop& loop) {
auto client = loop.resource<uvw::TcpHandle>();
client->on<uvw::ErrorEvent>([](const uvw::ErrorEvent&, const uvw::TcpHandle&) {
std::cerr << "Client connection failed.\n";
});
client->once<uvw::ConnectEvent>([client](const uvw::ConnectEvent&, uvw::TcpHandle&) {
std::cout << "Connected to server.\n";
auto msg1 = std::make_unique<char[]>('x');
auto msg2 = std::make_unique<char[]>("yz");
auto written1 = client->tryWrite(std::move(msg1), 1);
std::cout << "tryWrite bytes: " << written1 << "\n";
client->write(std::move(msg2), 2);
});
client->once<uvw::WriteEvent>([](const uvw::WriteEvent&, uvw::TcpHandle& cli) {
std::cout << "Message sent.\n";
cli.close();
});
client->once<uvw::CloseEvent>([](const uvw::CloseEvent&, const uvw::TcpHandle&) {
std::cout << "Client disconnected.\n";
});
client->connect("127.0.0.1", 8080);
}
void runExample() {
auto loop = uvw::Loop::getDefault();
startServer(*loop);
startClient(*loop);
loop->run();
}
Key Concepts
Event Handling with Lambdas
uvw replaces libuv’s functon pointers with template-based event registration using lambdas. For example:
handle->on<uvw::DataEvent>([](const uvw::DataEvent& evt, uvw::TcpHandle&) {
// Handle incoming data
});
Here, DataEvent is a template parameter that maps to libuv’s internal event type. The lambda receives two arguments: the event payload and the handle that triggered it. The handle is not a copy — it’s a reference to the original object, enabling direct manipulation.
tryWrite vs write
uvw exposes libuv’s uv_try_write and uv_write via two distinct methods:
tryWrite(): Attempts to send data immediately. Returns the number of bytes successfully written. If the OS buffer is full, it returns 0 or a negative error code — no queuing occurs.write(): Always queues the data. If the write cannot complete immediately, libuv buffers it and retries during the next event loop iteration.
This distinction gives developers fine-grained control over latency vs. reliability trade-offs.
Captured Variables with Initializers (C++14)
One of uvw’s elegant patterns uses lambda capture initializers to manage object lifetimes:
client->on<uvw::CloseEvent>([srv = server->shared_from_this()](const uvw::CloseEvent&, const uvw::TcpHandle&) {
srv->close();
});
Here, [srv = server->shared_from_this()] captures a shared_ptr to the server by initializing a new variable named srv inside the lambda’s capture list. This is a C++14 feature that allows capture variables to be initialized with arbitrary expressions — not just bound to existing variables.
This pattern ensures the server object remains alive as long as the lambda is referenced, preventing use-after-free bugs. Without this, the server might be destroyed before the client closes, leading to undefined behavior.
Resource Management with Smart Pointers
uvw avoids raw pointers entirely. Handles are created via loop.resource<T>(), which returns a std::shared_ptr<T>. This ensures automatic cleanup when handles go out of scope or are explicitly closed. The shared_from_this() idiom is used extensively to allow handles to reference themselves safely from within callbacks.
Loop and Handle Lifecycle
Every handle is bound to a loop. The loop must remain alive while handles are active. Calling loop->run() starts the event loop, which blocks until all active handles are closed. Once the last handle closes, the loop exits.
uvw’s design ensures that closing a handle automatically removes it from the loop’s internal tracking, making resource management predictable and exception-safe.