CORAL is a C++ library for building, connecting, and executing computational graphs. It provides a flexible framework for representing computational workflows as directed graphs where nodes represent data or operations, and edges represent dependencies. The library is designed with parallel scientific computing in mind.
git clone --recurse-submodules https://github.com/2listic/coral.git
In CORAL, every node is designed to be interpreted as a function, represented
by its operator(). This functional philosophy ensures that nodes are not
merely containers of data or operations but are active participants in the
computational graph. The execution of a node's function is determined by its
type, which defines its behavior and role within the graph.
-
Node as a Function: Each node encapsulates a callable function through its
operator(). When invoked, the node performs its designated operation, which may include constructing an object, modifying inputs, or producing outputs. -
Type-Driven Behavior: The behavior of a node during execution is controlled by its type. For example:
- Constructor Nodes: These nodes create new objects when executed.
- Pass-Through Nodes: These nodes modify their inputs and pass them to their outputs.
- Method Nodes: These nodes invoke a specific method on an object, potentially modifying its state or producing a result.
- Function Nodes: These nodes execute a free function, using their inputs as arguments and producing outputs.
-
Lazy Evaluation: Nodes are executed only when their outputs are explicitly required by other nodes or the user. This ensures that computations are performed efficiently and only when necessary.
-
Input and Output Management: Each node manages its inputs and outputs through a type-safe system. Inputs are connected to other nodes' outputs, and the execution of the node ensures that the outputs are updated accordingly.
This functional approach ensures that nodes in CORAL are versatile and adaptable, capable of representing a wide range of computational tasks. By interpreting nodes as functions, CORAL provides a consistent and intuitive framework for building and executing complex computational workflows.
The core design principles of CORAL are:
-
Type Safety: All connections between nodes are type-checked at runtime, ensuring that only compatible types can be connected.
-
Reflection System: The library implements a runtime reflection system that allows for introspection of types, methods, and functions, without requiring compiler support for C++ reflection.
-
Polymorphism Support: The system properly handles inheritance hierarchies, allowing derived types to be used where base types are expected.
-
Lazy Evaluation: Computation nodes are only executed when explicitly requested or when their outputs are needed by other nodes, allowing for efficient execution.
-
Serialization: All nodes can be serialized to and from JSON, enabling workflow persistence and reconstruction, and interfacing with graphical node editors and libraries.
The NodeObject class is the central component of CORAL. Each NodeObject:
- Wraps any C++ type using entt::meta_any and shared pointers
- Maintains type information through a hash-based type system
- Provides inputs and outputs for connecting to other nodes
- Can execute computation through its operator() method
Nodes are connected through a system of typed inputs and outputs:
- ConnectionType::input: Read-only parameters
- ConnectionType::output: Write-only results
- ConnectionType::pass_through: Parameters that are both read and modified
- ConnectionType::self: The node itself can be used as input and is returned as an output
The Network class manages collections of nodes and their connections,
allowing for the construction and execution of complex computational
workflows.
CORAL requires types to be registered before use. Registration functions include:
- register_elementary_type: For trivially copyable and constructible types
- register_type: For non-trivially copyable but trivially constructible types
- register_abstract_type: For interface types that can't be instantiated directly
- register_derived_type: For types inheriting from a base class
- register_method: For member functions (void/non-void, const/non-const)
- register_function: For free functions
The codebase is intentionally split so the core graph library can be built
without any particular “backend” (e.g. deal.II), and UIs can be built without
linking backend types. Backends provide types at runtime via a small plugin ABI
and/or by dumping a registry.json for UI authoring.
core/core/include/: CORAL public headers (NodeObject, Network, JSON, etc.)core/source/: CORAL core implementation + CLI sourcescore/include/coral_plugin.h: minimal C ABI that backend plugins export
backends/backends/dealii/: one backend implementation (deal.II)backends/dealii/src/plugin_dealii.cc: backend plugin (coral_backend_dealii)backends/dealii/include/register_types.h: deal.II type registrationbackends/dealii/tests/: backend-specific gtests
core/source/backend_main.cc: main CLI (coral)
The top-level CMakeLists.txt composes independent subprojects:
coral_core(always): the core library undercore/coral_backend_dealii: deal.II backend plugin underbackends/dealii/coral: main CLI undercore/
These options are enabled by default:
CORAL_BUILD_BACKEND_DEALII=ON(auto-skips ifdeal.IIis not found)CORAL_BUILD_TESTS=ON
Example:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 8External libraries (and translation units that include a lot of external headers, such as deal.II-heavy code) are built with warnings disabled to keep output clean by default.
CORAL “registration” is backend-specific: a backend is responsible for calling
coral::NodeObject::register_* for its own node types.
To add a new backend plugin (e.g. my_backend), create a new subdirectory and
provide two pieces: (1) a normal C++ registration function that calls
coral::NodeObject::register_*, and (2) a shared library that exports the CORAL
plugin ABI from core/include/coral_plugin.h.
Suggested layout:
backends/my_backend/include/register_types.h(declaresvoid register_types();)backends/my_backend/src/register_types.cc(implementsregister_types())backends/my_backend/src/plugin_my_backend.cc(exports the plugin ABI)backends/my_backend/CMakeLists.txt(builds the shared library target)
Minimal plugin entry points (backends/my_backend/src/plugin_my_backend.cc):
#include "coral_log.h" // instead of slog.h — do NOT link slog
#include "coral_plugin.h"
#include "register_types.h"
CORAL_PLUGIN_EXPORT const char *
coral_plugin_name()
{
return "my_backend";
}
CORAL_PLUGIN_EXPORT int
coral_load_plugin(const char *subjson, const CoralLogger *logger)
{
// Store the host logger so that coral_log_XXX macros become active.
coral_active_logger = logger;
coral_active_plugin_name = coral_plugin_name();
coral_log_info("Loading plugin.");
register_types();
return 0; // non-zero signals failure to the host
}
CORAL_PLUGIN_EXPORT void
coral_unload_plugin()
{
coral_log_info("Unloading plugin.");
}The host passes the optional JSON initialisation string and its own
CoralLogger to coral_load_plugin(). A non-zero return value is treated as
a load failure by the host.
CMake should build a shared library and link it against coral_core plus any
backend dependencies. The plugin must not link against slog directly:
add_library(coral_backend_my_backend SHARED ...)
target_link_libraries(coral_backend_my_backend PRIVATE coral_core ...)Plugins must not initialise or link slog themselves. Instead they use
core/include/coral_log.h, which provides macros that forward log calls to
the host's slog instance through the CoralLogger pointer received in
coral_load_plugin().
Available macros (mirror the slog levels):
| Macro | slog level |
|---|---|
coral_log(fmt, ...) |
SLOG_NOTAG |
coral_log_note(fmt, ...) |
SLOG_NOTE |
coral_log_info(fmt, ...) |
SLOG_INFO |
coral_log_warn(fmt, ...) |
SLOG_WARN |
coral_log_debug(fmt, ...) |
SLOG_DEBUG |
coral_log_error(fmt, ...) |
SLOG_ERROR |
coral_log_trace(fmt, ...) |
SLOG_TRACE (includes [file:line]) |
coral_log_fatal(fmt, ...) |
SLOG_FATAL (includes [file:line]) |
Every message is automatically prefixed with [<plugin_name>] using the value
returned by coral_plugin_name(). All macros are safe no-ops if
coral_active_logger is null (i.e. before coral_load_plugin() is called).
Use coral register (built under core/) to load a plugin and write the
registry JSON:
./build/core/coral --plugin ./build/backends/dealii/libcoral_backend_dealii.(dylib|so|dll) register plugin_init.json --registry-path registry.jsonHere in plugin_init.json the field plugin, if present, is passed as json to plugin for initialization. Coral is transparent to this initialization.
Core tests live in core/tests/. Backend-specific tests live next to the backend
(backends/dealii/tests/).
Instead of using ctest, the build provides a fast path to run the gtest
executable directly:
cmake --build build --target run_dealii_backend_testsRun core tests:
cmake --build build --target run_coral_core_testsOr run the binary yourself:
./build/backends/dealii/tests/dealii_backend_tests./build/core/tests/coral_core_testsThe execution model is based on the Taskflow library:
- Nodes are registered as tasks in a taskflow graph
- Dependencies between tasks are established based on node connections
- When execution is requested, tasks are run in the correct order (potentially in parallel)
- Results flow through the network as tasks complete
The library provides comprehensive JSON serialization for:
- Type information and relationships
- Node configurations and connections
- Input/output specifications
- Method and function registrations
This enables workflows to be saved, loaded, and shared between applications.
This section lists the public entry points, grouped by class and purpose. Each entry is shown with a short example. All symbols are referenced with their fully qualified names for Doxygen navigation.
- \ref coral::make_node (value or type)
- \ref coral::make_method_node (function or method)
coral::NodeObjectPtr a = coral::make_node(42);
coral::NodeObjectPtr b = coral::make_node<std::string>();
auto sum = [](int x, int y) { return x + y; };
coral::NodeObjectPtr f = coral::make_method_node("sum_ints", sum);- \ref coral::NodeObject::operator()()
- \ref coral::NodeObject::ready
- \ref coral::NodeObject::get
- \ref coral::NodeObject::get_shared
- \ref coral::NodeObject::parse_string
- \ref coral::NodeObject::to_string
coral::NodeObject::register_elementary_type<int>();
coral::NodeObjectPtr n = coral::make_node(7);
if (n->ready())
(void)(*n)();
int value = n->get<int>();
auto ptr = n->get_shared<int>();
std::string as_text = n->to_string();
n->parse_string("8");- \ref coral::NodeObject::get_input
- \ref coral::NodeObject::get_output
- \ref coral::NodeObject::bind_input
- \ref coral::NodeObject::bind_output
- \ref coral::NodeObject::bind_inputs
- \ref coral::connect
auto add = [](int a, int b) { return a + b; };
coral::NodeObject::register_function(add, {"add_ints", "sum", "a", "b"});
coral::NodeObjectPtr add_node = coral::make_method_node("add_ints", add);
coral::NodeObjectPtr a = coral::make_node(1);
coral::NodeObjectPtr b = coral::make_node(2);
coral::NodeObjectPtr out = coral::make_node(0);
add_node->bind_input(0, a);
add_node->bind_input(1, b);
add_node->bind_output(0, out);
coral::connect(add_node, {{a, 0}, {b, 0}});- \ref coral::NodeObject::is_bindable
- \ref coral::NodeObject::is_input_bound
- \ref coral::NodeObject::is_output_bound
- \ref coral::NodeObject::is_passthrough_input
- \ref coral::NodeObject::has_unbound_inputs
- \ref coral::NodeObject::has_unbound_outputs
bool can_bind = add_node->is_bindable(0);
bool in0 = add_node->is_input_bound(0);
bool out0 = add_node->is_output_bound(0);
bool pass0 = add_node->is_passthrough_input(0);
bool any_in = add_node->has_unbound_inputs();
bool any_out = add_node->has_unbound_outputs();- \ref coral::NodeObject::hash
- \ref coral::NodeObject::type_name
- \ref coral::NodeObject::node_type
- \ref coral::NodeObject::n_arguments
- \ref coral::NodeObject::n_inputs
- \ref coral::NodeObject::n_outputs
- \ref coral::NodeObject::get_info
- \ref coral::NodeObject::get_registry
std::string h = n->hash();
std::string tn = n->type_name();
coral::NodeType kind = n->node_type();
size_t args = n->n_arguments();
size_t ins = n->n_inputs();
size_t outs = n->n_outputs();
nlohmann::json info = n->get_info();
nlohmann::json registry = coral::NodeObject::get_registry();- \ref coral::NodeObject::register_elementary_type
- \ref coral::NodeObject::register_type
- \ref coral::NodeObject::register_abstract_type
- \ref coral::NodeObject::register_derived_type
- \ref coral::NodeObject::register_function
- \ref coral::NodeObject::register_method
- \ref coral::NodeObject::register_json_header
coral::NodeObject::register_elementary_type<int>();
coral::NodeObject::register_type<dealii::Triangulation<2>>();
struct Base {};
struct Derived : Base {};
struct Example
{
void set_value(int) {}
};
coral::NodeObject::register_abstract_type<Base>();
coral::NodeObject::register_derived_type<Base, Derived>();
auto fn = [](int x) { return x + 1; };
coral::NodeObject::register_function(fn, {"inc_int", "out", "in"});
coral::NodeObject::register_method(&Example::set_value,
{"set_value", "obj", "value"});
coral::NodeObject::register_json_header<int>("custom_int");- \ref coral::Network::add_node
- \ref coral::Network::add_connection
- \ref coral::Network::remove_nodes_and_connections
- \ref coral::Network::run
- \ref coral::Network::clear_network
coral::Network net;
auto id_a = net.add_node(coral::make_node(1.0), "a");
auto id_b = net.add_node(coral::make_node(2.0), "b");
auto id_sum = net.add_node(coral::make_method_node("sum_ints", sum), "sum");
net.add_connection(id_a, id_sum, 0, 0);
net.add_connection(id_b, id_sum, 0, 1);
net.run();- \ref coral::Network::get_inputs
- \ref coral::Network::get_outputs
- \ref coral::Network::get_input
- \ref coral::Network::get_output
- \ref coral::Network::get_node
- \ref coral::Network::get_node_name
- \ref coral::Network::set_node_name
- \ref coral::Network::n_nodes
- \ref coral::Network::n_connections
- \ref coral::Network::size
- \ref coral::Network::is_connected
- \ref coral::Network::get_connected_nodes
- \ref coral::Network::get_node_connections
- \ref coral::Network::get_registry
auto inputs = net.get_inputs();
auto outputs = net.get_outputs();
auto out0 = net.get_output(0);
auto node = net.get_node(id_sum);
net.set_node_name(id_sum, "sum");
bool linked = net.is_connected(id_a, id_sum);
auto targets = net.get_connected_nodes(id_a);
auto conns = net.get_node_connections(id_a);
nlohmann::json reg = net.get_registry();- \ref coral::Network::from_json
- \ref coral::Network::to_json
- \ref coral::Network::output_dot
nlohmann::json serialized = net.to_json();
coral::Network copy;
copy.from_json(serialized);
net.output_dot("network.dot");- \ref coral::Network::register_node
coral::Network::register_node();- \ref coral::register_non_dimensional_types
- \ref coral::register_dimensional_types
- \ref coral::register_all_types
coral::register_non_dimensional_types();
coral::register_dimensional_types<2, 2>();
coral::register_all_types();A typical workflow using CORAL involves:
- Registering relevant types, and dump the correpsonding json representation of the known node types
- Sending the JSON representation to a remote server (the frontend) for processing
- Creating nodes representing data or computational steps in the frontend, and connecting them to the appropriate inputs and outputs
- Send the resulting json to the backend for execution
- Executing the graph to perform the computation
The program coral has two subcommands (both require --plugin <path>):
register [register_path]: simply register all types and dump them toregister_path, a json file which defaults tonode_types.json.run [OPTIONS] input_json: register all types and run the graph described in the json fileinput_json. The options are:--register [register_path]: dump the types toregister_path, which defaults tonodes_type.json;--graph [graph_path]: dump the dot file of the network tograph_path, which defaults tonetwork.dot.--plugin <path>: path to the backend plugin to load.
Of course -h or --help to get a usage guide.
Luca Heltai luca.heltai@unipi.it