The dw::framework::Node interface prescribes a set of virtual functions which each node must implement. While not a requirement, the instructions apply the pointer to implementation (PIMPL) idiom to hide implementation details and reduce compilation dependencies.
The following subsections show how to implement a node named my_ns::MyNode
.
The MyNode
class needs to implement the dw::framework::Node interface. With the implementation being hidden in another class (called Impl) which is only known to the source file, all functions of the interface must be relayed to an Impl instance. During that relaying an additional functionality can be easily integrated: catching potential exceptions thrown in the Impl class and return dwStatus instead.
namespace my_ns { class MyNode : public dw::framework::ExceptionSafeProcessNode { ... } } // namespace my_ns
Every node must define a set of static functions used to introspect the interface of the node as well as to instantiate a node. These static functions are documented in dw::framework::NodeConcept.
The node constructor can have arbitrary arguments. Commonly they are structured in the following way:
struct MyNodeParams
.dwContextHandle_t
.MyNode(const MyNodeParams& params, const MyNodeRuntimeParams& runtimeParams, dwContextHandle_t ctx);
A factory function enables the instantiation of any node in a uniform way. The parameter provider can retrieve the parameter value for all parameters declared in the describeParameters() function. The information from the describeParameters() function also allows to map these parameter values to the necessary constructor arguments and members within a constructor argument.
static std::unique_ptr<MyNode> create(dw::framework::ParameterProvider&);
All input/output ports are enumerated in a user defined order. For each port the unique name and C++ data type must be declared. Additionally, optional flags can be specified, e.g. if a port must be bound to a channel in order for the node to be consider to be in a valid state.
static constexpr auto describeInputPorts() { return dw::framework::describePortCollection( DW_DESCRIBE_PORT(TypeFoo, "FOO"), DW_DESCRIBE_PORT(TypeBar, "BAR", dw::framework::PortBinding::REQUIRED), DW_DESCRIBE_PORT_ARRAY(TypeBaz, 3, "BAZ") ); } static constexpr auto describeOutputPorts() { return dw::framework::describePortCollection( DW_DESCRIBE_PORT(TypeOut, "OUT"), ); }
All passes are enumerated in their execution order. By convention the first pass is named SETUP and the last pass is named TEARDOWN. Beside the unique name the processor type where the computation is happening must be declared.
static constexpr auto describePasses() { return dw::framework::describePassCollection( dw::framework::describePass("SETUP", DW_PROCESSOR_TYPE_CPU), dw::framework::describePass("PROCESS", DW_PROCESSOR_TYPE_GPU), dw::framework::describePass("AGGREGATE", DW_PROCESSOR_TYPE_CPU), dw::framework::describePass("TEARDOWN", DW_PROCESSOR_TYPE_CPU), ); }
All parameters declare a mapping to constructor arguments or members within a constructor argument. While the constructor arguments must be declared in the same order as they appear in the constructor signature, the parameters within each are enumerated in a user defined order.
static constexpr auto describeParameters() { return dw::framework::describeConstructorArguments< MyNodeParams, MyNodeRuntimeParams, dwContextHandle_t, >( dw::framework::describeConstructorArgument( DW_DESCRIBE_..._PARAMETER(...), DW_DESCRIBE_..._PARAMETER(...), ... DW_DESCRIBE_..._PARAMETER(...) ), dw::framework::describeConstructorArgument( ... ), dw::framework::describeConstructorArgument( ... ) ); }
There are various kinds of parameters and for each there is a corresponding macro. All of these macros start with DW_DESCRIBE_
and end with PARAMETER
.
For all parameters the C++ type needs to be specified as the first argument which must match the variable type the value will be stored in. Optionally a semantic type can be provided (see "Parameter Details" under Concepts/Node).
The destination where the parameter value should be stored is specified at the end of the argument list.
describeConstructorArgument(DW_DESCRIBE_PARAMETER(..., &HelloWorldNodeParams::name))
in HelloWorldNode.hpp).DW_DESCRIBE_PARAMETER(..., &MyNodeParams::memberWhichIsAStruct, &MyNodeParamsSubStruct::nestedMember)
the value is stored in the nested member constructorArgument.memberWhichIsAStruct.nestedMember
.describeConstructorArgument(DW_DESCRIBE_UNNAMED_PARAMETER(dwContextHandle_t))
in HelloWorldNode.hpp) or for ABSTRACT
parameters where the destination is determined by custom logic.The signature of each of the following macros can be discovered by following the documentation of the macro to the function which is invoked by the macro.
DW_DESCRIBE_PARAMETER() / DW_DESCRIBE_PARAMETER_WITH_SEMANTIC()
The parameter value is read from JSON.
DW_DESCRIBE_ARRAY_PARAMETER() / DW_DESCRIBE_ARRAY_PARAMETER_WITH_SEMANTIC()
An array of such parameters.
DW_DESCRIBE_INDEX_PARAMETER() / DW_DESCRIBE_INDEX_PARAMETER_WITH_SEMANTIC()
The parameter value is read from JSON but since the JSON key contains an array an additional index is required to retrieve a specific item from that array.
DW_DESCRIBE_UNNAMED_PARAMETER() / DW_DESCRIBE_UNNAMED_PARAMETER_WITH_SEMANTIC()
Parameter value isn't coming from JSON but from a global singleton or some other context.
DW_DESCRIBE_UNNAMED_ARRAY_PARAMETER() / DW_DESCRIBE_UNNAMED_ARRAY_PARAMETER_WITH_SEMANTIC()
An array of such parameters.
DW_DESCRIBE_ABSTRACT_PARAMETER() / DW_DESCRIBE_ABSTRACT_ARRAY_PARAMETER()
The parameter value is read from JSON but not automatically stored in a constructor arguments or member within a constructor argument. Instead the node can use custom logic in the create()
function to utilize the parameter in a non-standard way (see below).
Beside primitive types or arrays thereof, a parameter can also be of enum type. For such an enum type a mapping must exist which correlates the integer value with a string representation. Commonly the identifier of each enumerator is being used. To define the mapping mentioned in the previous section, a specialization of the templated struct dw::framework::EnumDescription() needs to be defined.
For C enums this specialization can be places in the .cpp
of the concrete node using a parameter with that enum type.
template <> struct EnumDescription<MyEnum> { static constexpr auto get() { return describeEnumeratorCollection<MyEnum>( DW_DESCRIBE_C_ENUMERATOR(NAME_OF_ENUMERATOR_1), DW_DESCRIBE_C_ENUMERATOR(NAME_OF_ENUMERATOR_2), ... DW_DESCRIBE_C_ENUMERATOR(NAME_OF_ENUMERATOR_N) ); } };
For C++ enum classes this specialization should not be placed in the nodes sources but wherever the enum class definition is located.
template <> struct EnumDescription<MyEnum> { static constexpr auto get() { using EnumT = MyEnum; return describeEnumeratorCollection<EnumT>( DW_DESCRIBE_ENUMERATOR(NAME_OF_ENUMERATOR_1), DW_DESCRIBE_ENUMERATOR(NAME_OF_ENUMERATOR_2), ... DW_DESCRIBE_ENUMERATOR(NAME_OF_ENUMERATOR_N) ); } };
The base class constructor expects a unique_ptr to a node. As such an instance of the Impl class is passed which commonly has the same constructor signature as the concrete node.
MyNode::MyNode(const MyNodeParams& params, const MyNodeRuntimeParams& runtimeParams, dwContextHandle_t ctx) : dw::framework::ExceptionSafeNode(std::make_unique<MyNodeImpl>(params, runtimeParams, ctx)) {}
For the factory functions there are two implementation options:
std::unique_ptr<MyNode> MyNode::create(dw::framework::ParameterProvider& provider) { return dw::framework::create<MyNode>(provider); }
std::unique_ptr<MyNode> MyNode::create(dw::framework::ParameterProvider& provider) { auto constructorArguments = dw::framework::createConstructorArguments<MyNode>(); // to access individual constructor arguments by index: MyNodeParams& params = std::get<0>(constructorArguments); MyNodeRuntimeParams& runtimeParams = std::get<1>(constructorArguments); // arbitrary logic // e.g. reading two abstract parameters and using them together to populate the constructor argument size_t index; provider.getRequired("index", &index); // get an index from the JSON config file provider.getRequired("enabled", index, &(params.enable)); // extract a single flag from an array of flag using the index dw::framework::populateParameters<MyNode>(constructorArguments, provider); return dw::framework::makeUniqueFromTuple<MyNode>(std::move(constructorArguments)); }
Registering the node type allows it to be discovered and instantiated by its string name.
The registration with DW_REGISTER_NODE() should happen outside of the namespace and must use the fully qualified typename.
namespace my_ns { ... } DW_REGISTER_NODE(my_ns::MyNode)
The implementation class should inherit from a base class which not only implements the node interface but provides a default implementation for various functions. While the base class dw::framework::SimpleNode() provides all the necessary functionality, subclassing from dw::framework::SimpleNodeT() enables the usage of various macros later which can utilize the type of the node class.
class MyNodeImpl : public dw::framework::SimpleNodeT<MyNode> { ... }
The class should have a constructor with the same signature as the non-impl class.
MyNodeImpl(const MyNodeParams& params, const MyNodeRuntimeParams& runtimeParams, dwContextHandle_t ctx);
In order to provide default implementations with rich functionality for various functions, the dw::framework::SimpleNode() class needs to know about the ports and passes of the node. Those need to be initialize / registered and by convention that happens in a function init() which will be called in the constructor.
void init();
Additionally, for each pass (except the conventional ones SETUP and TEARDOWN) a method is declared to implement the logic of that pass. The methods can't take any argument, instead information can be passed between passes using member variables of the class.
dwStatus processPass(); dwStatus aggregatePass();
The constructor of the Impl class commonly stores the constructor arguments in member variables for later usage.
MyNodeImpl::MyNodeImpl(const MyNodeParams& params, const MyNodeRuntimeParams& runtimeParams, dwContextHandle_t ctx) : m_params(params), m_runtimeParams(runtimeParams), m_ctx(ctx) { init(); }
For the dw::framework::SimpleNode() base class to function properly, in the init()
function:
void MyNodeImpl::init() { NODE_INIT_INPUT_PORT("FOO"_sv); NODE_INIT_INPUT_PORT("BAR"_sv); NODE_INIT_INPUT_ARRAY_PORT("BAZ"_sv); TypeOut ref{}; NODE_INIT_OUTPUT_PORT("OUT"_sv, ref); NODE_REGISTER_PASS("PROCESS"_sv, [this]() { return processPass(); }); NODE_REGISTER_PASS("AGGREGATE"_sv, [this]() { return aggregatePass(); }); }
There is no need for the concrete impl class to contain any member variables for dw::framework::Port instance. Instead, these can be retrieved from the base class using one of the following macros by passing the port name and for array port additionally the index:
Similarly, there is no need to store dw::framework::Pass instances in member variables in the concrete node.
Implementation of the pass methods declared in the header with the desired logic.
The nodedescriptor tool can be used to generate the MyNode.node.json
file based on the information provided by the introspection API.