Skip to content

PabloPicose/SimplyNodeFramework

Repository files navigation

SimplyNodeFramework

Ubuntu Rocky Linux 8 Debian 12 Arch Linux

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.

Why use SimplyNodeFramework?

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.

Modules

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

Installation

Option A — git clone + add_subdirectory

# From your project root:
git clone https://github.com/PabloPicose/SimplyNodeFramework.git deps/SimplyNodeFramework
set(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)

Option B — FetchContent

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)

Option C — find_package after installation

cmake -S . -B build && cmake --build build
cmake --install build --prefix /usr/local
find_package(SNFCore CONFIG REQUIRED)
target_link_libraries(app PRIVATE SNFCore::SNFCore)

Quick start examples

Timer (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();
}

TCP echo server + client (SNFNetwork)

#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();
}

Cross-thread signal with moveToThread() (SNFCore)

#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.


Core Concepts

Node ownership

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 deletion

NodePtr<T> — safe references

Wraps a raw pointer + generation counter. Becomes false when the target is deleted, preventing use-after-free.

NodePtr<MyNode> ptr(node);
if (ptr) { ptr->doWork(); }

Signals and connections

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 and EventLoop

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

Timer t;
t.timeout.connect([]() { /* every 500 ms */ });
t.start(500ms);

Timer::singleShot(1000ms, []() { /* once */ });

Build options

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.


Guides

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

API Reference

Generate locally with Doxygen:

doxygen docs/Doxyfile
# open docs/html/index.html

TODO

General

  • 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

Core

  • 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)

Profiling

  • 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

Network

  • UDP socket
  • WebSocket
  • HTTP client
  • Serial port

Widgets

  • SelectionModel
  • ItemDelegate
  • Validator

Database

  • SQLite wrapper + SqlTableModel
  • MySQL wrapper

Audio

  • ALSA wrapper

Contributing

Contributions are welcome.

If you want to fix a bug, improve the documentation, or propose a new feature, please read the contribution guide first:

CONTRIBUTING.md

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors