nodl_generator_cpp¶
nodl_generator_cpp generates a C++ abstract base class from a NoDL document.
The generated class inherits from the appropriate node type (determined by includes), creates all endpoint handles
in its constructor, and exposes pure-virtual callbacks for inbound endpoints.
You subclass it and write only business logic.
Generated files are never edited by hand — they are regenerated whenever the .nodl.yaml changes.
This is the same generate-always pattern used by rosidl_generator_cpp and generate_parameter_library.
For what a NoDL document declares, see NoDL Concepts. This package implements the “forward” workflow: a NoDL document is the source of truth that makes a node’s interface exist.
CMake integration¶
The nodl_generate_cpp() CMake macro is the primary user-facing API.
Three lines in your CMakeLists.txt are the entire integration surface:
find_package(nodl_generator_cpp REQUIRED)
nodl_generate_cpp(my_node_base nodl/my_node.nodl.yaml)
add_executable(my_node src/my_node.cpp)
target_link_libraries(my_node PRIVATE my_node_base)
What the macro does¶
nodl_generate_cpp(TARGET NODL_FILE) creates a STATIC library target named TARGET that you link against.
It handles everything:
Step |
When |
What happens |
|---|---|---|
Dependency discovery |
Configure time |
Runs |
|
Configure time |
Automatically calls |
File watching |
Configure time |
Registers every file in the NoDL include tree as a |
Code generation |
Build time |
Runs the full generator via |
Library creation |
Build time |
Compiles the generated |
ROS linking |
Build time |
Links all ROS dependencies via |
Parameter library |
Build time |
When the document has parameters, links |
Arguments¶
Argument |
Description |
|---|---|
|
Name of the library target to create. Also used as the C++ class stem ( |
|
Path to the |
Rebuild behavior¶
Every file in the NoDL include tree — the root document and all transitive includes — is a configure-time dependency. A change to any of them triggers a CMake reconfigure, which re-evaluates the dependency information and reruns code generation. Subsequent builds skip generation entirely until a source file changes.
Cross-distro compatibility¶
The macro works across Humble through Lyrical.
It uses ${pkg}_TARGETS for linking (available since Foxy) and handles distro-specific target name changes
for generate_parameter_library dependencies (tl_expected::tl_expected on Humble/Jazzy vs tl::expected on
Kilted+, parameter_traits present on Humble/Jazzy but removed on Kilted+).
Prerequisites¶
The generator requires that a nodl document includes exactly one spec with a codegen class type BASE_CLASS.
The nodl_common_interfaces package provides NoDL descriptions for rclcpp::Node and rclcpp_lifecycle::LifecycleNode, which are of type BASE_CLASS. They are registered as nodl://rclcpp/node and nodl://rclcpp_lifecycle/lifecycle_node.
Add it as a dependency:
<depend>nodl_common_interfaces</depend>
This is a stopgap: once upstream packages ship their own .nodl.yaml, nodl_common_interfaces will be deprecated
and replaced by a direct dependency on the upstream package.
Generated files¶
The generator produces up to four files, depending on the document’s contents:
File |
Always |
Contents |
|---|---|---|
|
Yes |
Abstract base class header. |
|
Yes |
Constructor implementation — creates all handles. |
|
If parameters |
|
|
If parameters |
|
When using the CMake macro, a <target>_deps.cmake file is also written at configure time,
containing the NoDL source paths, ROS package dependencies, and generated file list.
Example¶
Given this NoDL input:
nodl_version: 2
include:
- ref: nodl://rclcpp/node
publishers:
- name: status
type: std_msgs/msg/String
qos:
history: KEEP_LAST
depth: 10
reliability: RELIABLE
subscriptions:
- name: cmd_vel
type: geometry_msgs/msg/Twist
qos:
history: KEEP_LAST
depth: 1
reliability: BEST_EFFORT
And this CMakeLists.txt:
find_package(ament_cmake REQUIRED)
find_package(nodl_generator_cpp REQUIRED)
nodl_generate_cpp(my_node nodl/my_node.nodl.yaml)
add_executable(my_node_exe src/my_node.cpp)
target_link_libraries(my_node_exe PRIVATE my_node)
ament_package()
The generator produces this header:
// GENERATED FILE — do not edit. Regenerated from NoDL by nodl_generator_cpp.
#pragma once
#include <memory>
#include <geometry_msgs/msg/twist.hpp>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
class MyNodeBase : public rclcpp::Node
{
public:
explicit MyNodeBase(const rclcpp::NodeOptions & options = rclcpp::NodeOptions{});
virtual ~MyNodeBase() = default;
protected:
// --- Publishers ---
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_status_;
// --- Subscription callbacks ---
virtual void on_cmd_vel(geometry_msgs::msg::Twist::ConstSharedPtr msg) = 0;
private:
// --- Subscriptions ---
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr sub_cmd_vel_;
};
And this source file:
// GENERATED FILE — do not edit. Regenerated from NoDL by nodl_generator_cpp.
#include "my_node.hpp"
MyNodeBase::MyNodeBase(const rclcpp::NodeOptions & options)
: rclcpp::Node("my_node", options)
{
// Create publishers
pub_status_ = this->create_publisher<std_msgs::msg::String>("status", rclcpp::QoS(10).reliable());
// Create subscriptions
sub_cmd_vel_ = this->create_subscription<geometry_msgs::msg::Twist>(
"cmd_vel",
rclcpp::QoS(1).best_effort(),
[this](geometry_msgs::msg::Twist::ConstSharedPtr msg) {
this->on_cmd_vel(msg);
});
}
The user subclasses MyNodeBase and implements on_cmd_vel():
#include "my_node.hpp"
class MyNode : public MyNodeBase
{
void on_cmd_vel(geometry_msgs::msg::Twist::ConstSharedPtr msg) override
{
// Business logic here
}
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MyNode>());
rclcpp::shutdown();
}
Generated class layout¶
The generated class uses visibility to separate concerns:
Visibility |
What |
Naming |
Why |
|---|---|---|---|
Protected |
Publishers |
|
Subclass needs |
Protected |
Service clients |
|
Subclass needs |
Protected |
Action clients |
|
Subclass needs |
Protected |
Parameter listener & params |
|
Subclass reads parameters. |
Protected, pure-virtual |
Subscription callbacks |
|
Subclass implements business logic. |
Protected, pure-virtual |
Service server callbacks |
|
Subclass implements business logic. |
Protected, pure-virtual |
Action server callbacks |
|
Subclass implements business logic. |
Private |
Subscription handles |
|
Wiring only — subclass has no reason to touch these. |
Private |
Service server handles |
|
Wiring only. |
Private |
Action server handles |
|
Wiring only. |
Entity names are sanitised for use as C++ identifiers: leading ~/ or / is stripped, remaining / becomes _.
Base class and provenance¶
The generator does not hardcode what class to inherit from. Instead, it walks the NoDL document’s include tree to determine the base class and to filter out entities that are already provided by an existing implementation.
The codegen.cpp metadata¶
A NoDL document can carry a codegen.cpp field declaring that it has an existing C++ implementation.
The schema for this field is defined in nodl_generator_cpp/nodl_generator_cpp/schemas/codegen_cpp.schema.yaml
and validated by nodl_generator_cpp, not nodl_schema.
For example, nodl://rclcpp/node (provided by nodl_common_interfaces) declares itself as a base-class provider:
# nodl://rclcpp/node
nodl_version: 2
codegen:
cpp:
role: BASE_CLASS
class: rclcpp::Node
header: rclcpp/rclcpp.hpp
publishers:
- name: /rosout
type: rcl_interfaces/msg/Log
qos: {history: KEEP_LAST, depth: 1, reliability: RELIABLE}
# ... /parameter_events, parameter services, use_sim_time, etc.
A consumer simply includes it — no codegen metadata of its own is needed:
# my_node.nodl.yaml
nodl_version: 2
include:
- ref: nodl://rclcpp/node
publishers:
- name: /status
type: std_msgs/msg/String
qos: {history: KEEP_LAST, depth: 10, reliability: RELIABLE}
Barriers and entity filtering¶
An included document that carries codegen.cpp is an implementation barrier.
All entities it declares — and all entities in documents it transitively includes — are provided: the existing
implementation already handles them, so the generator filters them out.
root (being generated — no codegen)
├── include: nodl://rclcpp/node [has codegen → barrier]
│ → /rosout, /parameter_events, … filtered out
└── own: /status scaffolded
The generator builds a provenance map: each entity maps to the codegen.cpp of its provider, or is absent (meaning
the generator must scaffold it).
Inheritance chains¶
A base-class provider can itself include another base class.
rclcpp_lifecycle::LifecycleNode extends rclcpp::Node:
root (being generated)
└── include: nodl://rclcpp_lifecycle/lifecycle_node [codegen: BASE_CLASS → barrier]
└── include: nodl://rclcpp/node [codegen: BASE_CLASS, behind barrier]
→ /rosout, /parameter_events, … all attributed to lifecycle_node
The inner rclcpp::Node sits behind LifecycleNode’s barrier, so all of Node’s entities are attributed to
LifecycleNode.
The generator sees exactly one base class — the outermost barrier — and inherits from it.
Error: multiple direct base classes¶
If the root document directly includes two unrelated base-class providers, the generator rejects the input — C++ single-inheritance means it cannot produce a class that inherits from two unrelated node types:
root
├── include: nodl://rclcpp/node [codegen: BASE_CLASS]
└── include: nodl://rclcpp_lifecycle/lifecycle_node [codegen: BASE_CLASS]
These are siblings; neither is behind the other’s barrier.
No base class¶
A document that does not include any codegen.cpp.role: BASE_CLASS provider is also an error.
Every generated node must inherit from a concrete base.
Parameters¶
NoDL parameters are compatible with generate_parameter_library
by design — the NoDL parameter schema is a formalization of genparamlib’s implicit schema.
The generator converts NoDL parameters to a genparamlib YAML file, then delegates to genparamlib to produce the
C++ parameter header.
No declare_parameter() calls appear in the generated templates.
The generated base class holds two protected members for parameter access:
protected:
my_node::ParamListener param_listener_;
my_node::Params params_;
Parameters declared by included documents behind a barrier (e.g. use_sim_time from rclcpp::Node) are filtered
out and do not appear in the genparamlib YAML or the generated header.
CLI reference¶
The CMake macro calls the generator internally, but it can also be used standalone for scripting or debugging.
Code generation¶
python -m nodl_generator_cpp \
--nodl-file my_node.nodl.yaml \
--output-dir generated/ \
--target-name my_node
Flag |
Required |
Description |
|---|---|---|
|
Yes |
Path to the NoDL document. |
|
Yes |
Directory to write generated files into (created if absent). |
|
Yes |
Used as the node name and the stem of all generated filenames. Must be a valid C++ identifier. |
Dependency discovery¶
python -m nodl_generator_cpp \
--nodl-file my_node.nodl.yaml \
--output-dir generated/ \
--target-name my_node \
--cmake-deps
The --cmake-deps flag runs the same load → provenance → filter pipeline as the full generator but stops before
template rendering.
It writes a <target>_deps.cmake file containing three CMake variables:
Variable |
Contents |
|---|---|
|
Absolute paths to the root NoDL file and every transitive include. |
|
Sorted, deduplicated ROS package names needed by the generated code. |
|
The filenames the full generator will produce. |
This is what the nodl_generate_cpp() CMake macro calls at configure time to set up find_package, file watching,
and the add_custom_command output list.
Relationship to other packages¶
The NoDL document consumed by this generator is validated by nodl_schema.
Include resolution and the document tree are provided by nodl_schema’s loader.
The codegen.cpp sub-object is opaque to nodl_schema — its schema and interpretation are owned entirely by this
package.
nodl_common_interfaces registers the base-class NoDL descriptions (nodl://rclcpp/node,
nodl://rclcpp_lifecycle/lifecycle_node) that the generator’s include references resolve against.
For registering a NoDL document with the ament index, see the ament_nodl package.