Hierarchy
Reactors can be composed hierarchically: any reactor can create and contain other reactors. This lets you build programs from well-defined building blocks and bundle related reactors into reusable components.
Creating and connecting nested reactors
In C++, nested reactors are declared as member variables of the container
reactor and initialized with Reactor::context()
instead of Environment::context(). Internal connections between nested
reactors are set up in assemble() using Reactor::connect().
Consider the following example. Copy it into a new file called
scaled_counter.cc.
#include <chrono>
#include <cstdint>
#include <iostream>
#include "xronos/sdk.hh"
namespace sdk = xronos::sdk;
using namespace std::literals::chrono_literals;
class Counter : public sdk::Reactor {
public:
using sdk::Reactor::Reactor;
[[nodiscard]] auto output() const -> const auto& { return output_; }
private:
sdk::PeriodicTimer timer_{"timer", context(), 100ms};
sdk::OutputPort<std::uint64_t> output_{"output", context()};
std::uint64_t count_{0};
class IncrementReaction : public sdk::Reaction<Counter> {
using sdk::Reaction<Counter>::Reaction;
Trigger<void> timer_trigger{self().timer_, context()};
PortEffect<std::uint64_t> output_effect{self().output_, context()};
void handler() final { output_effect.set(++self().count_); }
};
void assemble() final { add_reaction<IncrementReaction>("increment"); }
};
class Printer : public sdk::Reactor {
public:
using sdk::Reactor::Reactor;
[[nodiscard]] auto input() const -> const auto& { return input_; }
private:
sdk::InputPort<std::uint64_t> input_{"input", context()};
class PrintReaction : public sdk::Reaction<Printer> {
using sdk::Reaction<Printer>::Reaction;
Trigger<std::uint64_t> input_trigger{self().input_, context()};
void handler() final {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
self().get_time_since_startup());
std::cout << self().name() << " received " << *input_trigger.get()
<< " at " << elapsed << '\n';
}
};
void assemble() final { add_reaction<PrintReaction>("print"); }
};
class Gain : public sdk::Reactor {
public:
Gain(std::string_view name, const sdk::Context& context, std::uint64_t factor)
: sdk::Reactor(name, context)
, factor_(factor) {}
[[nodiscard]] auto input() const -> const auto& { return input_; }
[[nodiscard]] auto output() const -> const auto& { return output_; }
private:
std::uint64_t factor_;
sdk::InputPort<std::uint64_t> input_{"input", context()};
sdk::OutputPort<std::uint64_t> output_{"output", context()};
class ScaleReaction : public sdk::Reaction<Gain> {
using sdk::Reaction<Gain>::Reaction;
Trigger<std::uint64_t> input_trigger{self().input_, context()};
PortEffect<std::uint64_t> output_effect{self().output_, context()};
void handler() final { output_effect.set(*input_trigger.get() * self().factor_); }
};
void assemble() final { add_reaction<ScaleReaction>("scale"); }
};
class ScaledCounter : public sdk::Reactor {
public:
ScaledCounter(std::string_view name, const sdk::Context& context, std::uint64_t factor)
: sdk::Reactor(name, context)
, gain_("gain", this->context(), factor) {}
[[nodiscard]] auto output() const -> const auto& { return output_; }
private:
Counter counter_{"counter", context()};
Gain gain_;
sdk::OutputPort<std::uint64_t> output_{"output", context()};
void assemble() final {
connect(counter_.output(), gain_.input());
connect(gain_.output(), output_);
}
};
auto main() -> int {
sdk::Environment env{};
ScaledCounter scaled{"scaled_counter", env.context(), 3};
Printer printer{"printer", env.context()};
env.connect(scaled.output(), printer.input());
env.execute();
return 0;
}
Gain is a simple reactor that multiplies every incoming value by a
configurable factor. Because factor is a constructor argument, Gain
provides its own constructor instead of inheriting one with
using sdk::Reactor::Reactor.
ScaledCounter is a container reactor that composes Counter and Gain into
a single component. Both sub-reactors are declared as member variables and
initialized with Reactor::context() so that
the runtime knows they belong to ScaledCounter. Because Gain needs a
factor argument, gain_ cannot be initialized with a simple in-class
initializer — the factor value is only available at construction time.
ScaledCounter therefore accepts factor in its own constructor and passes it
through to gain_.
ScaledCounter also declares its own OutputPort.
Inside assemble(),
connect(gain_.output(), output_) passes values produced by gain_ through to
the outside world. A container’s input port can be forwarded inward in the same
way. From the outside, ScaledCounter looks like any other reactor with an
output port.
In the same directory, create a CMakeLists.txt:
cmake_minimum_required(VERSION 3.28)
project(hierarchy LANGUAGES CXX)
set(XRONOS_SDK_VERSION "0.11.2" CACHE STRING "Xronos SDK version")
include(FetchContent)
FetchContent_Declare(
"xronos-sdk"
URL https://github.com/xronos-inc/xronos/releases/download/v${XRONOS_SDK_VERSION}/xronos-sdk-${XRONOS_SDK_VERSION}-Linux-${CMAKE_SYSTEM_PROCESSOR}.tar.gz
)
FetchContent_MakeAvailable(xronos-sdk)
set(xronos-sdk_DIR ${xronos-sdk_SOURCE_DIR}/share/cmake/xronos-sdk)
find_package(xronos-sdk)
add_executable(scaled_counter scaled_counter.cc)
target_link_libraries(scaled_counter xronos::xronos-sdk)
Configure, build, and run:
$ cmake -B build
$ cmake --build build --target scaled_counter
$ ./build/scaled_counter
Running the program produces output similar to the following:
$ ./build/scaled_counter
printer received 3 at 0ms
printer received 6 at 100ms
printer received 9 at 200ms
printer received 12 at 300ms
printer received 15 at 400ms
printer received 18 at 500ms
printer received 21 at 600ms
printer received 24 at 700ms
printer received 27 at 800ms
...
Use Ctrl+C to stop the program.
Reactions accessing nested ports
Instead of (or in addition to) using connect(), a container reactor can define
its own reactions that interact directly with its sub-reactors’ ports. This is
useful when the container needs to apply logic that goes beyond simple
forwarding.
Consider the following modification of ScaledCounter:
class ScaledCounter : public sdk::Reactor {
public:
ScaledCounter(std::string_view name, const sdk::Context& context, std::uint64_t factor)
: sdk::Reactor(name, context)
, gain_("gain", this->context(), factor) {}
[[nodiscard]] auto output() const -> const auto& { return output_; }
private:
Counter counter_{"counter", context()};
Gain gain_;
sdk::OutputPort<std::uint64_t> output_{"output", context()};
class OnGainReaction : public sdk::Reaction<ScaledCounter> {
using sdk::Reaction<ScaledCounter>::Reaction;
Trigger<std::uint64_t> gain_trigger{self().gain_.output(), context()};
PortEffect<std::uint64_t> output_effect{self().output_, context()};
void handler() final {
auto value = *gain_trigger.get();
if (value % 2 != 0) {
output_effect.set(value);
}
}
};
void assemble() final {
connect(counter_.output(), gain_.input());
add_reaction<OnGainReaction>("on_gain");
}
};
ScaledCounter contains the same Counter and Gain sub-reactors as before
and wires them together in assemble().
The difference is that instead of connecting gain_.output() directly to
output_, it registers an OnGainReaction.
The reaction declares self().gain_.output() as its trigger,
so it runs every time Gain produces a value. It also holds a
port effect on self().output_, which gives
the handler a way to write to the container’s own output port. The handler reads
the scaled value and only forwards it when it is odd, effectively halving the
output rate.
Running this modified program produces output similar to the following:
$ ./build/scaled_counter
printer received 3 at 0ms
printer received 9 at 200ms
printer received 15 at 400ms
printer received 21 at 600ms
printer received 27 at 800ms
...
Use Ctrl+C to stop the program.

