C++ API
The nano-ros C++ API is a freestanding C++14 interface for ROS 2 nodes on embedded and host targets. It needs no standard library, no exceptions and no RTTI, so the same code builds for Zephyr, FreeRTOS, NuttX, ThreadX, bare metal and Linux.
- Upstream’s names, in upstream’s namespaces.
rclcpp::Node,Publisher<M>,Subscription<M>,Service<S>,Client<S>,Timer,GuardCondition,Executor, andrclcpp_action::Server<A>/Client<A>. Every user-facing name is inrclcpp::,rclcpp_action::orrclcpp_lifecycle::; there is nonros::namespace. The extensions with no upstream counterpart (thePoll*types, the*Storagetypes,FixedString, …) are inrclcpp::too. - Errors are values. Every fallible call returns
rclcpp::Result, andRCLCPP_TRYreturns early on failure. Nothing throws. - Generated message types.
std_msgs::msg::Int32,example_interfaces::srv::AddTwoIntsand so on, generated by the build from the packages your node depends on (see Message Generation). - An opt-in std surface for ported code. Defining
NROS_CPP_STDadds thestd::string/std::function/std::chronooverloads and thestd::shared_ptr-returningrclcpp::Nodefactories. The build requests it; it is never detected from the toolchain. See Two surfaces.
The headers live in the SDK’s C++ include directory at upstream’s paths:
rclcpp/, rclcpp_action/ and rclcpp_lifecycle/. rclcpp/detail/ is
internal.
To build and run a first C++ node, start with First Node — C++; to grow it into a workspace, see C++ in a workspace. To bring an existing ROS 2 node across, see Porting a ROS 2 C++ node. This page is the API itself.
API reference
C++ API Doxygen — start here.
The Doxygen site is generated from the current main branch. The umbrella
header rclcpp/rclcpp.hpp pulls in every
public surface a user application needs.
Where to start
rclcpp/rclcpp.hpp— convenience umbrella includerclcpp::init_in/rclcpp::shutdown— session lifetimerclcpp::Node— node + create_publisher/subscription/service/client/action_*rclcpp::Publisher<M>/Subscription<M>. Hosted, a subscription callback may also take upstream’sconst rclcpp::MessageInfo&as its second argument (see Subscription)rclcpp::Service<S>/Client<S>— the DISPATCH service server and client. A handler is registered into the executor arena, which owns the entity.Service<S>::SharedPtrisrclcpp::ServiceHandle<S>, a two-word keep-alive with nothing to dereference;Client<S>::SharedPtrisrclcpp::ClientHandle<S>, the same two words with one verb,async_send_request. Neither has anoperator->.rclcpp::PollService<S>— L1 polling service server. The caller owns the server and drains it withtake_request/send_responsefrom its own spin loop.node.create_service<S>(name)with no handler hands one back.rclcpp::PollClient<S>— L1 future-style service client, the client half of the same split. The caller owns the client and drains the reply itself:send_requestreturns arclcpp::Future<T>(on a hosted buildasync_send_request(shared_ptr)returns upstream’sFutureAndRequestId—.future,.request_id,share()),callblocks while driving the executor,call_pollingblocks without driving it, andwait_for_service/service_is_readyprobe the server.node.create_client<S>(name)with no handler hands one back.rclcpp_action::Server<A>/Client<A>— L2 callback model (executor-arena registered).rclcpp::ActionServer<A>/rclcpp::ActionClient<A>are the same two types. Hosted, upstream’s goal handles too (see Actions with upstream’s goal handles).rclcpp::PollingActionServer<A>/PollingActionClient<A>— L1 polling model. Caller drivestake_*/accept_goal/complete_goalfrom a spin loop.rclcpp::Future<T>— async result handlerclcpp::Executor,Timer,GuardCondition- Parameters are methods ON the node —
declare_parameter<T>/get_parameter<T>/set_parameter<T>/has_parameter, upstream’s names, forwarding to the one store thercl_interfacesservices read.rclcpp::Seq<T, N>is the array VALUE a freestanding node declares with (std::vector<T>underNROS_CPP_STD). There is no standalonerclcpp::ParameterServer: a second store would be invisible toros2 param get. See Differences from ROS 2 §9 for what is intentionally smaller thanrclcpp, and Parameters below.
Two-layer API. The
rclcpp::ActionServer<A>/ActionClient<A>templates are L2 callback handles, dispatched by the executor. Therclcpp::PollingActionServer<A>/PollingActionClient<A>are L1 polling templates with the same typed serialization glue but caller-driven control flow. Subscriptions, services and service clients have both shapes too (Subscription/PollSubscription, and so on). See Two-Layer API for the per-layer rationale.
Event-driven wake callbacks.
PollingActionServer<A>andPollingActionClient<A>expose a nestedWakeStatePOD and typedset_{goal,cancel,get_result,goal_response, cancel_response,result,feedback}_wake_callback(state, cb, ctx)methods. Pair annros_cpp_wake_state_twith each entity / channel to wake on rx instead of polling.
Core API
The snippets below use the freestanding surface. Each create_* writes the
entity through an out-reference and returns a Result.
Initialization
#include <rclcpp/rclcpp.hpp>
// Global init (simple applications). `init_in` is the Result-returning form:
// upstream's `rclcpp::init(argc, argv)` returns `void`, so the name differs
// where the contract does.
rclcpp::Result ret = rclcpp::init_in("tcp/127.0.0.1:7447", 0);
// Or an explicit executor (multi-executor patterns)
rclcpp::Executor executor;
RCLCPP_TRY(rclcpp::Executor::create(executor, "tcp/127.0.0.1:7447"));
Node
rclcpp::Node node;
RCLCPP_TRY(rclcpp::create_node(node, "my_node"));
// Or with an explicit executor:
RCLCPP_TRY(executor.create_node(node, "my_node", "/namespace"));
Publisher
#include "std_msgs.hpp"
rclcpp::Publisher<std_msgs::msg::Int32> pub;
RCLCPP_TRY(node.create_publisher(pub, "/chatter"));
std_msgs::msg::Int32 msg;
msg.data = 42;
RCLCPP_TRY(pub.publish(msg));
A publisher can be marked express (it bypasses transmit batching) through its QoS profile; see TX Throughput & Latency Tuning.
Subscription
A poll-style subscription is rclcpp::PollSubscription<M>: the caller owns the
subscriber, so the caller is the one that takes from it. Call spin_once() to
drive I/O, then take() to check for a message. rclcpp::Subscription<M> is
the DISPATCH subscription, whose samples the executor arena delivers to a
callback.
rclcpp::PollSubscription<std_msgs::msg::Int32> sub;
RCLCPP_TRY(node.create_subscription(sub, "/chatter"));
rclcpp::spin_once(100);
std_msgs::msg::Int32 msg;
if (sub.take(msg).ok()) {
printf("Received: %d\n", msg.data);
}
On a hosted build (NROS_CPP_STD) a dispatch subscription’s callback may take
upstream’s const rclcpp::MessageInfo& as a second argument, after the message
in any of its four forms (const M&, std::shared_ptr<const M>,
std::shared_ptr<M>, std::unique_ptr<M>):
auto sub = node->create_subscription<std_msgs::msg::Int32>(
"/chatter", 10, [](const std_msgs::msg::Int32& msg, const rclcpp::MessageInfo& info) {
const auto& r = info.get_rmw_message_info();
printf("%d from seq %llu at %lld ns\n", msg.data,
(unsigned long long)r.publication_sequence_number, (long long)r.source_timestamp);
});
get_rmw_message_info() carries source_timestamp, received_timestamp,
publication_sequence_number, reception_sequence_number and the 24-byte
publisher_gid, as the backend reported them. A backend that reports none
(XRCE, uORB) leaves every field zero. from_intra_process is always false.
A freestanding build refuses the shape at compile time: the metadata path keeps
the callable on the heap. SubscriptionOptions::message_info = true with a
callback that takes no MessageInfo aborts the create, because it would
otherwise configure nothing.
Service server
rclcpp::PollService<S> is the polled server: the caller owns it and drains
it. rclcpp::Service<S> is the DISPATCH server, which takes a handler.
#include "example_interfaces.hpp"
using AddTwoInts = example_interfaces::srv::AddTwoInts;
rclcpp::PollService<AddTwoInts> srv;
RCLCPP_TRY(node.create_service(srv, "/add_two_ints"));
// In your main loop:
rclcpp::spin_once(10);
AddTwoInts::Request req;
int64_t seq;
if (srv.take_request(req, seq).ok()) {
AddTwoInts::Response resp;
resp.sum = req.a + req.b;
srv.send_response(seq, resp);
}
Service client
rclcpp::PollClient<S> is the polled client: the caller owns it and drains
the reply. rclcpp::Client<S> is the DISPATCH client, whose one verb is
async_send_request and whose reply reaches a handler during spin_once.
rclcpp::PollClient<AddTwoInts> client;
RCLCPP_TRY(node.create_client(client, "/add_two_ints"));
AddTwoInts::Request req;
req.a = 1; req.b = 2;
AddTwoInts::Response resp;
RCLCPP_TRY(client.call(req, resp));
// resp.sum == 3
Action server
rclcpp_action::Server<A> is a DISPATCH server: the executor answers goal and
cancel requests through callbacks you install, and the goal itself is driven
through the server by its 16-byte id. Upstream hands handle_accepted an owning
std::shared_ptr<ServerGoalHandle>; here the goals live in the server’s static
arena, so the callback receives the id instead. A polled form, where the caller
takes each goal request itself, is rclcpp::PollingActionServer<A>.
#include "example_interfaces.hpp"
using Fibonacci = example_interfaces::action::Fibonacci;
rclcpp_action::Server<Fibonacci> srv;
RCLCPP_TRY(node.create_action_server(srv, "/fibonacci"));
// Stateless callables: an empty-capture lambda or a plain function pointer.
// The `*_with_ctx` forms add a `void*` for state.
RCLCPP_TRY(srv.set_goal_callback([](const uint8_t id[16], const Fibonacci::Goal& goal) {
return goal.order > 0 ? rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE
: rclcpp_action::GoalResponse::REJECT;
}));
RCLCPP_TRY(srv.set_accepted_callback([](const uint8_t id[16]) {
// Record `id`; the work happens outside the callback, which must return
// promptly.
}));
// Later, from the main loop, for an accepted goal `goal_id`:
Fibonacci::Feedback fb;
// ... fill feedback ...
srv.publish_feedback(goal_id, fb);
Fibonacci::Result result;
// ... fill result ...
srv.complete_goal(goal_id, result); // GoalStatus::Succeeded; another overload takes a status
Action client
rclcpp_action::Client<A> keeps its goal, feedback and result buffers (and
the four transport channels underneath) in fixed-size storage inside the
Client<A> instance itself. Nothing is heap-allocated per send_goal call,
and the type is move-only.
Blocking convenience. send_goal() and get_result() spin the executor
internally until the server replies or the per-call timeout expires:
rclcpp_action::Client<Fibonacci> client;
RCLCPP_TRY(node.create_action_client(client, "/fibonacci"));
Fibonacci::Goal goal;
goal.order = 10;
uint8_t goal_id[16];
RCLCPP_TRY(client.send_goal(goal, goal_id));
Fibonacci::Result result;
RCLCPP_TRY(client.get_result(goal_id, result));
These helpers are sugar over send_goal_async() + spin_once(). They cannot
be called from inside a dispatch callback; a blocking call made there fails
with ErrorCode::Reentrant.
Future-based async. send_goal_future() / get_result_future() return a
Future<T> keyed by its goal: each future receives only its own goal’s
acceptance or result, whether the executor dispatched it during spin_once or
the future’s own poll took it, and a result for a goal nobody waits on is
dropped. At most MAX_CONCURRENT_GOALS of each may be pending per client;
past that the send is refused and the future is empty (valid() false):
auto fut = client.send_goal_future(goal);
typename decltype(client)::GoalAccept accept;
RCLCPP_TRY(fut.wait(executor.handle(), 5000, accept));
if (accept.accepted) {
auto rfut = client.get_result_future(accept.goal_id);
Fibonacci::Result result;
RCLCPP_TRY(rfut.wait(executor.handle(), 10000, result));
}
GoalAccept is a nested type on rclcpp_action::Client<A> (16-byte UUID +
bool accepted).
Feedback. Feedback is not goal-scoped at the stream layer:
feedback_stream() yields FeedbackType across every active goal for this
client.
auto& fb_stream = client.feedback_stream();
Fibonacci::Feedback fb;
while (fb_stream.try_next(fb).ok()) { /* ... */ }
// Or blocking:
RCLCPP_TRY(fb_stream.wait_next(executor.handle(), 500, fb));
For per-goal feedback dispatch, use the callback API.
Callback API: SendGoalOptions + set_callbacks(). This is the
rclcpp-style entry point. SendGoalOptions is a nested POD on
rclcpp_action::Client<A>; populate the three function-pointer fields
(goal_response, feedback, result) plus an optional context pointer,
then install once. Callbacks fire from spin_once():
typename decltype(client)::SendGoalOptions opts;
opts.goal_response = [](bool accepted, const uint8_t id[16], void* ctx) {
auto* state = static_cast<MyState*>(ctx);
state->accepted = accepted;
std::memcpy(state->goal_id, id, 16);
};
opts.feedback = [](const uint8_t id[16], const uint8_t* data, size_t len, void* ctx) {
Fibonacci::Feedback fb;
if (Fibonacci::Feedback::ffi_deserialize(data, len, &fb) == 0) {
// dispatch on `id`
}
};
opts.result = [](const uint8_t id[16], int32_t status, const uint8_t* data, size_t len, void* ctx) {
// status: 4=Succeeded, 5=Canceled, 6=Aborted
};
opts.context = &my_state;
RCLCPP_TRY(client.set_callbacks(opts));
RCLCPP_TRY(client.send_goal_async(goal, goal_id)); // fire-and-forget
while (!my_state.done) { rclcpp::spin_once(10); }
set_callbacks() may be called before or after send_goal_async(): the
executor reads the latest callbacks on each dispatch. The client also keeps the
most recent feedback and result bytes, so the same Client can drive
feedback_stream().try_next() and get_result_future().wait() even with
callbacks installed.
Actions with upstream’s goal handles (hosted)
On a hosted build (NROS_CPP_STD) the stock rclcpp_action spellings are
drop-ins over the two classes above:
using Fibonacci = example_interfaces::action::Fibonacci;
using GoalHandle = rclcpp_action::ServerGoalHandle<Fibonacci>;
using namespace std::placeholders;
// Server: free factory over the node interfaces, std::bind-ed members.
rclcpp_action::Server<Fibonacci>::SharedPtr server = rclcpp_action::create_server<Fibonacci>(
get_node_base_interface(), get_node_clock_interface(), get_node_logging_interface(),
get_node_waitables_interface(), "fibonacci",
std::bind(&MyNode::handle_goal, this, _1, _2), // (const GoalUUID&, shared_ptr<const Goal>)
std::bind(&MyNode::handle_cancel, this, _1), // (shared_ptr<GoalHandle>)
std::bind(&MyNode::handle_accepted, this, _1)); // (shared_ptr<GoalHandle>)
// in handle_accepted: goal_handle->get_goal(), ->publish_feedback(fb),
// ->is_canceling(), ->succeed(result) / ->abort(result) / ->canceled(result)
// Client: SendGoalOptions' upstream fields, bound per goal by async_send_goal.
auto client = rclcpp_action::create_client<Fibonacci>(this, "fibonacci");
auto opts = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
opts.goal_response_callback = [](rclcpp_action::ClientGoalHandle<Fibonacci>::SharedPtr h) {};
opts.feedback_callback = [](auto h, const std::shared_ptr<const Fibonacci::Feedback> fb) {};
opts.result_callback = [](const rclcpp_action::Client<Fibonacci>::WrappedResult& r) {
// r.code: rclcpp_action::ResultCode::SUCCEEDED / ABORTED / CANCELED; r.result
};
auto fut = client->async_send_goal(goal, opts); // rclcpp::Future<GoalAccept>
// The handle-taking calls return upstream's std::shared_future.
auto result = client->async_get_result(goal_handle); // shared_future<WrappedResult>
auto cancel = client->async_cancel_goal(goal_handle); // shared_future<CancelResponse::SharedPtr>
if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS) {
const auto& wrapped = result.get();
}
// server, after handle_goal returned ACCEPT_AND_DEFER:
goal_handle->execute(); // ACCEPTED -> EXECUTING, status array published
X<A>::SharedPtr is rclcpp::Owned<X<A>> (the entity by value), and the
get_node_*_interface() handles name the node and do nothing else. A goal
handle is a heap token over the goal id: every call forwards to the server or
client keyed by it, and the goal’s state stays in the arena. Drive it from the
executor’s task only — not from a thread you started — and do not move the
server while a handle is in use. The callbacks run from the executor’s
dispatch, beside the client-wide set_callbacks ones. async_send_goal
returns the goal-keyed rclcpp::Future<GoalAccept>, not a
std::shared_future; get_goal_stamp is absent.
async_get_result / async_cancel_goal / async_cancel_all_goals /
async_cancel_goals_before return a
rclcpp_action::detail::DrivenSharedFuture<T>, which IS-A
std::shared_future<T>: its is_ready() (what spin_until_future_complete
polls), wait_for and get drive the client’s dispatch and executor while
they wait, since no other thread fulfils it. Converted to a bare
std::shared_future it still completes, but only when something else drives
the client. Every async_get_result for one goal returns the handle’s own
future; a result-aware goal’s handle is kept by the client until its result
arrives, as upstream’s pending request keeps it. Cancel replies are taken by
the executor’s spin, as upstream’s executor delivers them: the spin settles
the waiting async_cancel_* future and runs its cancel_callback, so a
caller that keeps only the callback needs no poll(). A reply no future
claims is kept for try_recv_cancel_response, oldest first, up to
rclcpp_action::CANCEL_STASH_CAP (4) per client. These need std::promise,
so they are hosted-only twice over: a libstdc++ built without threads
(arm-none-eabi) refuses them as a freestanding build does.
ServerGoalHandle::execute() and Server<A>::execute(goal_id) move an
ACCEPTED goal to EXECUTING and publish the status array; the latter reports
NotFound / NotAllowed where upstream raises. create_server executes an
ACCEPT_AND_EXECUTE goal before handle_accepted; the goal-id API leaves it
ACCEPTED.
Goal slots. Each action sub-client holds at most MAX_CONCURRENT_GOALS (4)
requests in flight. A future dropped (or cancel()ed) before its reply frees
its request’s slot, unless something else still waits on that reply — a
client-wide set_callbacks hook, or an upstream goal (async_send_goal with
callbacks, async_get_result). A late reply for a freed slot is dropped. On a
freestanding build each of these shapes is a compile error
(NROS_RCLCPP_REFUSE_ACTION_GOAL_HANDLE) naming the goal-id-keyed form above;
create_client alone works there.
Timer
Timers fire during spin_once():
void on_timer(void* ctx) {
// periodic work
}
rclcpp::Timer timer;
RCLCPP_TRY(node.create_wall_timer(timer, 1000, on_timer)); // 1000 ms period
RCLCPP_TRY(node.create_timer_oneshot(timer, 5000, on_timer)); // one-shot after 5 s
timer.cancel();
timer.reset(); // restart from zero
GuardCondition
Guard conditions allow cross-thread signalling:
void on_signal(void* ctx) {
// handle event
}
rclcpp::GuardCondition guard;
RCLCPP_TRY(node.create_guard_condition(guard, on_signal));
// From another thread:
guard.trigger();
// Callback fires on the next spin_once()
Executor
rclcpp::Executor executor;
RCLCPP_TRY(rclcpp::Executor::create(executor));
rclcpp::Node node;
RCLCPP_TRY(executor.create_node(node, "my_node"));
// Create publishers, subscriptions, etc. on node...
while (executor.ok()) {
executor.spin_once(10);
}
executor.shutdown();
Spinning
// Global spin (after rclcpp::init_in())
rclcpp::spin_once(10); // single poll, 10 ms timeout
rclcpp::spin_in(5000); // spin for 5 seconds
rclcpp::spin_in(5000, 50); // spin for 5 s, 50 ms poll interval
rclcpp::spin_in(); // until rclcpp::shutdown() flips rclcpp::ok()
// Explicit executor
executor.spin_once(10);
executor.spin_for(5000, 50); // bounded; executor.spin() blocks until shutdown
How spinning maps onto RTOS tasks and priorities is the subject of Execution Model.
Parameters and /parameter_events
Parameters are methods on the node (declare_parameter<T>, get_parameter<T>,
set_parameter<T>, undeclare_parameter), over the executor’s one store.
node.start_parameter_services() serves them on the six rcl_interfaces
services, so ros2 param reaches them, and publishes /parameter_events
(rcl_interfaces/msg/ParameterEvent, rmw_qos_profile_parameter_events) as
rclcpp’s node does:
rclcpp::Node node;
RCLCPP_TRY(executor.create_node(node, "ctrl"));
node.declare_parameter<int64_t>("gain", 3); // before or after: both reported
RCLCPP_TRY(node.start_parameter_services()); // start-up event: new_parameters = [gain]
node.set_parameter<int64_t>("gain", 4); // changed_parameters = [gain]
node.undeclare_parameter("gain"); // deleted_parameters = [gain]
Each event names the node’s fully qualified name in node; the events are
published on the next spin. The image needs the param_services capability.
One publisher per executor serves every node. It is on for every served node
unless the node was built with
rclcpp::NodeOptions().start_parameter_event_publisher(false), which opts
that one node out; the other nodes keep publishing.
To react to OTHER nodes’ changes, a hosted build has
rclcpp::ParameterEventHandler. It subscribes to /parameter_events once, on
the node you give it:
auto handler = std::make_shared<rclcpp::ParameterEventHandler>(node);
auto h = handler->add_parameter_callback(
"gain", [](const rclcpp::Parameter& p) { use(p.as_int()); }, "/controller");
auto e = handler->add_parameter_event_callback(
[](const rcl_interfaces::msg::ParameterEvent& ev) { /* every event */ });
Keep the handles: dropping one unregisters it, as upstream’s do. An empty node
name means this node, and a relative one resolves against this node’s
namespace. A parameter callback sees new and changed values, never deletions.
An event callback receives the type it takes, so rcl_interfaces must be
generated for the image. The per-parameter handle is
rclcpp::ParameterCallbackHandle, as upstream (the handler’s nested
ParameterEventHandler::ParameterCallbackHandle names the same type). A
freestanding build refuses the handler at compile time; there, subscribe to
/parameter_events with create_subscription yourself.
Error handling
All fallible operations return rclcpp::Result. Use RCLCPP_TRY for early
return:
rclcpp::Result setup() {
RCLCPP_TRY(rclcpp::init_in());
RCLCPP_TRY(rclcpp::create_node(node, "my_node"));
RCLCPP_TRY(node.create_publisher(pub, "/topic"));
return rclcpp::Result::success();
}
Check results manually when needed:
rclcpp::Result ret = pub.publish(msg);
if (!ret.ok()) {
printf("Error: %d\n", ret.raw());
}
ret.raw() is an rclcpp::ErrorCode. The numbering matches rcl_ret_t:
| Code | Value | Meaning |
|---|---|---|
Ok | 0 | success |
Error | -1 | generic failure not covered by a more specific code |
Timeout | -2 | the deadline elapsed before completion |
InvalidArgument | -3 | null pointer, empty topic name, or out-of-range value |
NotFound | -4 | entity not found (topic, parameter, service, goal) |
AlreadyExists | -5 | duplicate declare or register |
Full | -6 | a static pool is exhausted (executor slots, subscription buffers, …) |
NotInitialized | -7 | init was never called, or the entity is in its default state |
BadSequence | -8 | the call is invalid in the current state |
ServiceFailed | -9 | a service request or reply failed |
PublishFailed | -10 | publish failed |
SubscriptionFailed | -11 | subscription create or take failed |
NotAllowed | -12 | not allowed for this entity or backend |
Rejected | -13 | the peer considered the request and declined (a rejected goal, a QoS mismatch) |
TryAgain | -14 | no data ready yet (non-blocking take); retry later |
Reentrant | -15 | a blocking call was made from inside a callback |
Unsupported | -16 | not implemented by the active backend |
TransportError | -100 | the transport rejected the operation |
A Full from a create_* usually means a build-time pool is too small; see
Static Pool Inventory for the knob that sizes it.
Two surfaces: freestanding, and NROS_CPP_STD
One set of headers ships two API shapes, and a build picks one.
The freestanding surface is the default and it is not a fallback. It is
plain C++14 with no standard library, no exceptions and no RTTI:
rclcpp::Node, create_* writing through an out-reference and returning an
rclcpp::Result, const char* for names, uint64_t period_ms for durations.
It is the surface for every node written for nano-ros, native host builds
included.
The std surface is an opt-in and it is reached only by defining
NROS_CPP_STD. Defining it turns on the six NROS_CPP_HAS_* capability
macros in rclcpp/detail/std_detect.hpp, and with them: the std::string /
std::function / std::chrono / std::vector convenience overloads in
nros/std_compat.hpp, the RCLCPP_*_STREAM logging macros, and — the piece
that actually motivates the flag — rclcpp::Node’s upstream-shaped
factories, which return std::shared_ptr and take const std::string&
names.
Do I need this flag?
The split is ported rclcpp code versus code written for nano-ros. It is not host versus embedded. A native Linux build does not want the flag any more than a Cortex-M3 build does; a ported node wants it on both.
| You are… | Flag | Why |
|---|---|---|
writing a new node against rclcpp::Node / rclcpp::Publisher<M> / rclcpp::Executor | no | The freestanding surface is the whole API you are using. This is the common case on every platform, native included. |
compiling an upstream ROS 2 .cpp unmodified — it says class MyNode : public rclcpp::Node, std::make_shared<MyNode>(), create_publisher<M>(...) returning a SharedPtr | yes | Those signatures are spelled in std::shared_ptr and std::string. Without the flag they do not exist and you get a compile error naming a missing overload. |
passing a std::string topic name or a std::chrono period into an otherwise nano-ros-native file | yes | Those are the std_compat.hpp overloads. |
building for Zephyr, FreeRTOS, NuttX, ThreadX, or any -ffreestanding toolchain | no | And here it is not merely unnecessary: asking for the std surface asks the headers to #include <string>, which on those toolchains is either absent or a hard #error. |
If you are unsure, build without it. The failure direction is loud: a missing overload at compile time, never a silent behaviour change.
Turning it on
Define it on the target, not per file:
target_compile_definitions(my_ported_node PRIVATE NROS_CPP_STD=1)
Per target rather than per #define-before-include, because the macro changes
the layout of rclcpp::Node (it carries hosted-only members and an
enable_shared_from_this base), so two translation units of one image that
disagree about it are an ODR/layout break rather than a missing function. An
image may set the flag on one module and not another, so keep the definition
at target granularity and give every translation unit of a target the same
answer.
A ported package usually needs none of this. nano-ros’s ament surface sets
NROS_CPP_STD=1 on every target its ament_* verbs create, and
find_package(rclcpp) sets it on whatever links rclcpp::rclcpp, so a ported
package already has the surface it needs; see
Porting a ROS 2 C++ node.
The ported shape
With the flag set, an upstream node’s own spelling compiles: std::make_shared,
SharedPtr returns, a std::string topic, a QoS depth, a capturing lambda and
a std::chrono period.
#include <rclcpp/rclcpp.hpp>
#include "std_msgs/std_msgs.hpp"
using namespace std::chrono_literals;
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("talker");
auto pub = node->create_publisher<std_msgs::msg::Int32>("chatter", 10);
int count = 0;
auto timer = node->create_wall_timer(1s, [&]() {
std_msgs::msg::Int32 msg;
msg.data = ++count;
pub->publish(msg);
});
rclcpp::spin(node);
rclcpp::shutdown();
}
A failed create_* here ABORTS, naming the call, because upstream throws and
nano-ros has no exceptions. To handle the failure instead, use the
Result-returning forms above.
Why it is not detected for you
Earlier releases discovered the capability macros from the include path with
__has_include, so a hosted compiler handed you the std-flavoured API whether
you asked for it or not. That was removed because no probe answers the
question correctly on both embedded toolchains:
__STDC_HOSTED__ | __has_include(<string>) | #include <string> | |
|---|---|---|---|
arm-none-eabi 13.2, -ffreestanding (FreeRTOS) | 0 | TRUE | hard #error |
Zephyr -nostdinc++, minimal libcpp | 1 | FALSE | absent |
| hosted g++ 12.3 | 1 | TRUE | works |
Each probe is right on exactly one embedded toolchain and wrong on the other:
libstdc++ 13 added bits/requires_hosted.h, so header presence stopped
implying usability. Only the request is right on both, because it is not a
probe — it is you saying which surface you want. The full measurement is in
the comments of rclcpp/detail/std_detect.hpp.
Zephyr
A Zephyr application reaches the C++ API through the nano-ros Zephyr module.
The module, the image’s system.toml and the prj.conf split are covered in
Zephyr integration; the RMW,
locator and domain come from the image, never from CONFIG_NROS_* lines in
prj.conf. The C++ side needs CONFIG_CPP=y and CONFIG_STD_CPP14=y in
prj.conf (Zephyr’s own settings), and the application’s CMakeLists.txt
generates the message bindings:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_app LANGUAGES CXX)
nros_generate_interfaces(std_msgs LANGUAGE CPP)
target_sources(app PRIVATE src/main.cpp)
A minimal main() waits for the network, opens the session with the locator
and domain the image rendered into Kconfig, and spins:
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <nros/platform_zephyr.h>
#include <rclcpp/rclcpp.hpp>
#include "std_msgs.hpp"
LOG_MODULE_REGISTER(my_app, LOG_LEVEL_INF);
int main(void)
{
nros_platform_zephyr_wait_network(CONFIG_NROS_INIT_DELAY_MS);
rclcpp::Result ret = rclcpp::init_in(CONFIG_NROS_ZENOH_LOCATOR, CONFIG_NROS_DOMAIN_ID);
if (!ret.ok()) return 1;
rclcpp::Node node;
if (!rclcpp::create_node(node, "my_node").ok()) return 1;
// ... create publishers, subscriptions, etc.
while (true) {
rclcpp::spin_once(100);
}
}
nros_platform_zephyr_wait_network (<nros/platform_zephyr.h>) is defined
in every Zephyr image. On an image with no IP stack (CONFIG_NET_SOCKETS off,
for example a serial-only zenoh image) it returns 0 at once, so the call needs
no guard. CONFIG_NROS_INIT_DELAY_MS is the module’s start-up delay; your
main() is its only reader.
Building the API docs from source
The Doxygen site linked above is the published copy. Building it from the nano-ros source tree: see Internals → Build Commands.
Design background
- RFC-0018: C++ API design — freestanding C++14, no exceptions, the
Resultchannel - RFC-0089: ROS 2 API adoption, and the compile-or-conform rule — upstream’s namespaces and names, the
_inrule, the hosted surface - RFC-0021: blocking API rules — why blocking calls drive the executor and refuse re-entry