Simplicity is a key idea in this project. The goal is to make life easier for whoever is using the library.
For example, the library includes Widgets that can be used both in desktop applications and web applications — we will see an example later — and it is designed so that, with just a simple flag, your application can run directly in a browser, full UI included.
A modular C++ library providing an event-driven, node-based runtime.
Requires C++17. Primary target is Linux (epoll); WebAssembly (Emscripten) is supported for SNFCore.
Sometimes you just want to have an application that runs multithreaded, without having to worry about threads. Maybe you want to write an application that starts a TCP/WebSocket server and an HTTP server, and you want everything to be multithreaded. And if you come from the Qt world, you probably want something with a signals and slots system, but you also want to compile your application statically on Linux without having to perform dark magic.
This library is simple from the user's point of view. As simple as it could reasonably be made, so that anyone who knows how to program in C++ but thinks a bit like a Python or JavaScript developer can use it. You do not always want an application that needs to squeeze every last millisecond out of the machine. Most of the time, I am pretty sure you do not need to hunt down that extra 1 ms of optimization. But you may still want the speed that C++ gives you, combined with the ease of use you get from a framework like Qt or Node.js. That is what this library is trying to offer.
As a tiny example, suppose we are going to build a TCP server, and every client that connects will receive back the same message it sent — your classic echo server — but we also want it to be multithreaded, just for the sake of the example.
We could bring in Boost.Asio and deal with a thousand templates, or bring in Qt and deal with licenses and heavy compilation. On top of that, those libraries are very, very large, and they are designed to do a huge number of things that we may not actually need.
Most people would probably end up with a Python script generated by AI, and honestly, that is perfectly fine. But I like typed languages, and I like the observer design pattern — signals and slots — so let us implement our server. We are going to send each client to our EventLoop without worrying about it. All we know is that the clients will live on different threads, and that the framework will make sure that if we have 4,000 clients, it will not create 4,000 threads. Instead, they will be magically distributed across the system:
#include <SNFCore/Application.h>
#include <SNFNetwork/TcpServer.h>
#include <SNFNetwork/TcpSocket.h>
using namespace snf;
int main(int argc, char** argv)
{
// Our application, always the first thing
Application app(argc, argv);
// Non-blocking server
TcpServer server;
server.listen(HostAddress::LocalHost, 9000);
// Every time a client connects, this lambda is executed
// on the server thread (the current thread, so it is safe to access its methods)
server.newConnection.connect([&]() {
TcpSocket* peer = server.nextPendingConnection();
// Every time our client "peer" tells us something, we send it back
peer->readyRead.connect([peer]() { peer->write(peer->readAll()); });
// If our client wants to disconnect, we safely delete it.
peer->disconnected.connect([peer]() { peer->deleteLater(); });
// Now we are going to forget about our client "peer" and throw it into a thread system.
// This is where the magic happens.
peer->moveToThreadPool();
// Here is the magic: the framework has a pool of sleeping threads, and it also keeps track
// of how long each "loop" takes to process. So this client will go to the thread with the
// lowest workload, and all its events (the slots) will run on that thread.
//
// In this specific example it may not make a huge difference, but we could have a TcpSocket
// subclass with its own variables, and whenever something happens, those variables would be
// safe to use inside all its slots.
});
// We call the application's run() method. It will handle a kind of infinite loop for us,
// and the best part is that when we are done, everything will be safely cleaned up.
return app.run();
}And there we go. We already have a multithreaded TCP server in about 20 lines of code, comments included.
To be fair, thanks to AI we have also added quite a lot of functionality, backed by plenty of tests, including coverage tests.
| Package | Contents | Dependencies |
|---|---|---|
SNFCore |
Event loop, node ownership tree, timers, signals, cross-thread dispatch | pthread |
SNFNetwork |
TCP/Unix sockets (non-blocking, epoll-based). Linux only. | SNFCore |
SNFWidgets |
Dear ImGui + GLFW integration, main loop abstraction for desktop and WebAssembly | SNFCore, ImGui, GLFW |
SNFDatabase |
SQLite wrapper and SqlTableModel |
SNFCore, SQLite |
SNFSnmp |
SNMP client and agent implementation (Linux only) | SNFCore, net-snmp |
SNFHttpServer |
HTTP server and request/response parser (Linux only) | SNFCore |
SNFWebSocket |
WebSocket client and server implementation (Linux and WebAssembly) | SNFCore |
SNFJson |
JSON wrapper around nlohmann/json | SNFCore, nlohmann_json |
# From your project root:
git clone https://github.com/PabloPicose/SimplyNodeFramework.git deps/SimplyNodeFrameworkset(SNF_ENABLE_TESTS OFF CACHE BOOL "" FORCE)
set(SNF_ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(deps/SimplyNodeFramework)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE SNFCore::SNFCore)include(FetchContent)
if(NOT TARGET SNFCore::SNFCore)
set(SNF_ENABLE_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
SimplyNodeFramework
GIT_REPOSITORY https://github.com/PabloPicose/SimplyNodeFramework.git
GIT_TAG main
)
FetchContent_MakeAvailable(SimplyNodeFramework)
endif()
add_executable(app main.cpp)
target_link_libraries(app PRIVATE SNFCore::SNFCore)cmake -S . -B build && cmake --build build
cmake --install build --prefix /usr/localfind_package(SNFCore CONFIG REQUIRED)
target_link_libraries(app PRIVATE SNFCore::SNFCore)#include <SNFCore/Application.h>
#include <SNFCore/Timer.h>
#include <iostream>
using namespace snf;
using namespace std::chrono_literals;
int main(int argc, char** argv)
{
Application app(argc, argv);
int ticks = 0;
Timer timer;
timer.timeout.connect([&]() {
std::cout << "Tick " << ++ticks << "\n";
if (ticks >= 5) { timer.stop(); app.quit(); }
});
timer.start(200ms);
return app.run();
}#include <SNFCore/Application.h>
#include <SNFCore/Timer.h>
#include <SNFNetwork/HostAddress.h>
#include <SNFNetwork/TcpServer.h>
#include <SNFNetwork/TcpSocket.h>
#include <iostream>
using namespace snf;
using namespace std::chrono_literals;
int main(int argc, char** argv)
{
Application app(argc, argv);
TcpServer server;
server.listen(HostAddress::LocalHost, 9000);
server.newConnection.connect([&]() {
TcpSocket* peer = server.nextPendingConnection();
peer->readyRead.connect([peer]() { peer->write(peer->readAll()); });
peer->disconnected.connect([peer]() { peer->deleteLater(); });
});
TcpSocket client;
client.connectToHost(HostAddress::LocalHost, 9000);
client.connected.connect([&]() { client.write("Hello, SNF!\n"); });
client.readyRead.connect([&]() {
auto b = client.readAll();
std::cout << std::string(b.begin(), b.end());
app.quit();
});
client.errorOccurred.connect([&](const std::string& e) {
std::cerr << e << "\n"; app.quit();
});
Timer::singleShot(3000ms, [&]() { app.quit(); });
return app.run();
}#include <SNFCore/Application.h>
#include <SNFCore/Connection.h>
#include <SNFCore/Node.h>
#include <SNFCore/NodePtr.h>
#include <SNFCore/Timer.h>
#include <future>
#include <iostream>
#include <thread>
using namespace snf;
class Receiver : public Node {
public:
explicit Receiver(Node* p = nullptr) : Node(p) {}
void slotRecv(int v) {
std::cout << "Received " << v << " on thread " << std::this_thread::get_id() << '\n';
Application::instance()->quit();
}
private:
void update() override {}
};
int main(int argc, char** argv)
{
Application app(argc, argv);
std::promise<std::thread::id> ready;
std::thread worker([&]() {
auto* loop = app.getOrCreateCurrentThreadEventLoop();
ready.set_value(std::this_thread::get_id());
Timer keepAlive; keepAlive.setSingleShot(true); keepAlive.start(500);
loop->run();
});
auto* receiver = new Receiver();
receiver->moveToThread(ready.get_future().get());
NodePtr<Receiver> ptr(receiver);
Signal<int> signal;
signal.connect(ptr, &Receiver::slotRecv, ConnectionType::Queued);
std::cout << "Emitting from main thread " << std::this_thread::get_id() << '\n';
signal.emit(42);
app.run();
worker.join();
return 0;
}For a detailed explanation see docs/guides/thread-affinity.md.
Every Node belongs to a parent–child ownership tree. The parent destructs its
children. Root nodes (no parent) are managed by the Application.
auto* child = new MyNode(parent); // parent owns child
node->deleteLater(); // safe deferred deletionWraps a raw pointer + generation counter. Becomes false when the target is
deleted, preventing use-after-free.
NodePtr<MyNode> ptr(node);
if (ptr) { ptr->doWork(); }ConnectionType |
Delivery |
|---|---|
Direct (default) |
Synchronous on the emitter's thread |
Queued |
Posted to the receiver's EventLoop |
Signal<int> sig;
sig.connect([](int v) { /* direct */ });
sig.connect(NodePtr<Worker>(w), &Worker::onValue, ConnectionType::Queued);Application app(argc, argv);
return app.run(); // blocks until quit()
// Register a new thread and post work to it:
app.getOrCreateCurrentThreadEventLoop();
loop->post([]() { /* runs on this thread */ });Timer t;
t.timeout.connect([]() { /* every 500 ms */ });
t.start(500ms);
Timer::singleShot(1000ms, []() { /* once */ });| CMake variable | Default | Description |
|---|---|---|
SNF_ENABLE_TESTS |
ON if top-level |
Build unit tests (GoogleTest) |
SNF_ENABLE_EXAMPLES |
ON if top-level |
Build in-tree examples |
SNF_WEB_ASSEMBLY |
OFF |
Build SNFCore for WebAssembly (requires Emscripten toolchain) |
SNF_ENABLE_WIDGETS |
ON |
Build SNFWidgets (Dear ImGui + GLFW) |
SNF_ENABLE_SQLITE |
OFF |
Build SNFDatabase with SQLite support |
SNF_ENABLE_MYSQL |
OFF |
Build SNFDatabase with MySQL support |
SNF_ENABLE_SNMP |
OFF |
Build SNFSnmp with net-snmp support |
top-level means the root CMakeLists.txt is being configured, as opposed to being included via add_subdirectory() or FetchContent. This allows downstream projects to disable tests and examples when including SNF as a dependency.
Step-by-step documentation lives in docs/guides/:
| Guide | Description |
|---|---|
| Building with Emscripten | Compile an SNFWidgets app to WebAssembly and run it in a browser or Node.js — no platform #ifdefs needed |
| Thread Affinity | Cross-thread signal delivery with moveToThread() and Queued connections |
| Logging | Emit structured log messages, configure the central logger, and plug in custom sinks |
Generate locally with Doxygen:
doxygen docs/Doxyfile
# open docs/html/index.html- Ini parser
- JSON wrapper (nlohmann/json)
- HTTP server + request/response parser
- TLV Packet helper (
[magic:4][flags:1][type:2][len:4][payload:N]) - Embed static assets into binary
- File system watcher (inotify)
- Process management (spawn, stdout/stderr capture)
- Plugin system (runtime shared-library loading)
- Per-node update flag (skip
update()when no work is pending) - Logging framework (log levels, formatting, file output)
- Classes and macros for measuring execution time, memory usage, and other metrics
- Server profiler that collects and exposes metrics via a socket or HTTP endpoint
- Macro/function to measure momory usage of a class or function scope
- UDP socket
- WebSocket
- HTTP client
- Serial port
- SelectionModel
- ItemDelegate
- Validator
- SQLite wrapper + SqlTableModel
- MySQL wrapper
- ALSA wrapper
Contributions are welcome.
If you want to fix a bug, improve the documentation, or propose a new feature, please read the contribution guide first: