Introduction
GNode is a lightweight and extensible C++ framework for building node-based graphs. It allows you to define nodes, connect them through input/output ports, and process data through a directed graph structure. GNode is designed to be:
- Modular: you create only the nodes you need
- Type-safe: strong typing of ports and attributes
- Extensible: supports custom types as long as they are copyable or movable
A graph in GNode is composed of:
- Nodes: units of logic or data
- Ports: typed inputs/outputs on nodes
- Links: connections between ports
- Graph (supervisor): manager for node creation, registration, and update
Installation
See the README in the GitHub repository:
https://github.com/otto-link/GNode
Usage Example
#include <iostream>
struct Vec2
{
float x;
float y;
Vec2() = default;
Vec2(float x, float y) : x(x), y(y) {}
};
{
public:
{
}
{
}
};
{
public:
{
}
{
}
};
{
public:
{
}
{
if (p_in1 && p_in2) *p_out = *p_in1 + *
p_in2;
}
};
{
public:
{
}
{
if (p_vec) *p_sum = p_vec->x + p_vec->y;
}
};
{
public:
{
}
{
if (p_in) std::cout << "PRINTING: " << *p_in << "\n";
}
};
int main()
{
auto id_value_vec = g.
add_node<ValueVec>(1.f, 2.f);
g.
new_link(id_value_vec,
"value", id_sum_vec,
"vec");
auto id_value1 = g.
add_node<Value>(3.f);
g.
new_link(id_sum_vec,
"sum", id_add1,
"a");
g.
new_link(id_value1,
"value", id_add1,
"b");
g.
new_link(id_add1,
"a + b", id_print1,
"in");
auto id_value2 = g.
add_node<Value>(4.f);
g.
new_link(id_add1,
"a + b", id_add2,
"a");
g.
new_link(id_value2,
"value", id_add2,
"b");
g.
new_link(id_add2,
"a + b", id_print2,
"in");
std::cout << "\nOVERALL UPDATE\n";
std::cout << "\nNODE UPDATE\n";
return 0;
}
The Graph class provides methods for manipulating nodes and connections in a directed graph structure...
Definition graph.hpp:35
bool new_link(const std::string &from, int port_from, const std::string &to, int port_to)
Connect two nodes in the graph using port indices.
virtual void update()
Mark all nodes as dirty and update the entire graph.
void export_to_mermaid(const std::string &fname="export.mmd", const std::string &graph_label="graph")
Export the graph to a Mermaid file.
void export_to_graphviz(const std::string &fname="export.dot", const std::string &graph_label="graph")
Export the graph to a Graphviz DOT file.
virtual std::string add_node(const std::shared_ptr< Node > &p_node, const std::string &id="")
Add a new node to the graph.
T * get_node_ref_by_id(const std::string &node_id) const
Get a pointer to a node by its ID.
Definition graph.hpp:217
Abstract Node class that represents a basic building block in a graph-based system.
Definition node.hpp:34
T * get_value_ref(const std::string &port_label) const
Get a reference to the value stored in a port by its label.
Definition node.hpp:223
virtual void compute()=0
Pure virtual function that forces derived classes to implement the compute method,...
Main header aggregating the gnode library.
@ IN
Represents an input port.
Definition port.hpp:34
@ OUT
Represents an output port.
Definition port.hpp:35
Architecture
Overview
- Nodes hold logic and expose typed input/output ports
- Ports transport typed data objects
- Links connect an output port of one node to an input port of another
- Graphs manage nodes, links, evaluation order, and data propagation
The library is minimal, dependency-free except for <memory>, <vector>, and optional spdlog.
Key Components
BaseData and Data<T> (in data.hpp)
Responsibility
Represent typed, mutable values transported through ports.
Structure
BaseData
Data<T>
- Stores a value of type
T
- Provides:
get_value_ref()
- Type-safe clone
- Access via
T* for internal generic port-level handling
- Value set by the
GNode::Node class: GNode::Node::set_value
Role in architecture
Data<T> is the foundation of type-safety at the port level. Every output port owns its Data<T>, and input ports bind to the same instance when linked.
Ports - InputPort and OutputPort (in port.hpp)
Ports connect nodes.
Port
- Knows:
- Port name
- Node ID
- Port ID inside the node
OutputPort<T>
- Owns a
shared_ptr<Data<T>>
- Always produces data
- Can be connected to any number of
InputPort
InputPort<T>
- Does not own data
- Holds a pointer to a
Data<T> owned by an OutputPort
- If unconnected, the data pointer is
nullptr
- Can be disconnect to only one
OutputPort
Role
Ports are the typed interface between nodes. InputPorts never store data, they alias data from the upstream OutputPort.
Link (in link.hpp)
A Link is a simple POD struct:
std::string from_node_id
std::string to_node_id
int from_port_index
int to_port_index
Purpose
Represents a connection between two ports in a graph, used by Graph to physically link port objects.
Node (in node.hpp)
The most important unit.
Responsibilities
- Represent a computational unit
- Own ports
- Implement specific logic
- Expose:
- label
- id
- input ports list
- output ports list
Important API
add_port<T>(port_type, label, ...)
compute() — pure virtual: each node defines its logic
T *get_value_ref<T>(port_label)
Design Pattern
Nodes use templates to declare typed ports, but evaluation remains virtual (dynamic).
Graph (in graph.hpp)
The orchestrator.
Responsibilities
- Store nodes (
std::map<std::string, shared_ptr<Node>>)
- Store links
- Resolve and connect ports
- Provide update/evaluation sequences
Important methods
add_node<...>(...): add a new node of a specific type to the graph (template-based)
new_link(from_id, out_port_idx, to_id, in_port_idx) - connections from output to input it "one to many"
update() - mark all nodes as dirty and update the entire graph
update(node_id) - update a specific node by its ID and propagate modifications to other nodes
remove_link(...)
remove_node(id)
T* get_node_ref_by_id(id)
Evaluation Strategy
The graph does implement a dependency solver and topological sorting. The user can decide not to use the provided update() methods and define its own scheduling policy.
Architecture Diagram
+--------------+ link +--------------+
| Node A |------------>| Node B |
| | | |
| [out:T]-----+ +-----[in:T] |
+--------------+ +--------------+
^
|
Data<T> stored in OutputPort
|
InputPort references the same Data<T>
Dataflow Model
- A node computes some output in
compute()
- All connected
InputPorts see the same Data<T> pointer
- Downstream nodes read inputs during their own
compute()
- In the
compute() node method, data are accessed by reference:
void compute()
{
float *p_in1 = this->get_value_ref<float>("a");
float *p_in2 = this->get_value_ref<float>("b");
float *p_out = this->get_value_ref<float>("a + b");
if (p_in1 && p_in2) *p_out = *p_in1 + *p_in2;
}
Repository