rclcpp_kit — cheat sheet for a coding agent¶
You are writing Python that drives ROS 2 core (rclcpp + tf2 + rosbag2) through
rclcpp_kit, via cppyy. The kit mirrors the C++ API and hides only the cppyy
friction (bringup, symbol resolution, message conversion, ordered teardown). It is
the capability layer every ROS-touching kit builds on; it is not the rclcppyy
drop-in accelerator (that's the separate rclcppyy product, which re-exports this).
(For why this exists and the measured TF numbers, see WHY.md / REPORT.md.)
Requires the ROS core, present in the default ros-base env. Its own env is
rclcpp: pixi run -e rclcpp python your_script.py.
Golden rules
- Call rclcpp = rclcpp_kit.bringup_rclcpp() once; it returns the real rclcpp
namespace and is idempotent. The first call JITs rclcpp/rclcpp.hpp (a few
seconds) — do it at startup, not on a hot path.
- A plain rclcpp.Node accepts both calling conventions: rclpy-style
(node.create_publisher(String, "topic", 10), Python messages auto-converted to
C++) and native rclcpp template syntax (node.create_publisher[CppMsgT]("topic", 10),
zero-overhead). Same for create_subscription / create_timer.
- Message classes: hand either a Python message class (std_msgs.msg.String) or a
cppyy C++ class (cppyy.gbl.std_msgs.msg.String) — the kit resolves both.
- Let teardown happen: cppyy_kit.shutdown() runs at interpreter exit and releases
the rclcpp context in order (no os._exit needed). A normal return/sys.exit
is clean.
Pattern 1 — bring up rclcpp, publish/subscribe the rclpy way¶
Use for: any node that needs the C++ backend with familiar rclpy calls.
import rclcpp_kit
from std_msgs.msg import String
rclcpp = rclcpp_kit.bringup_rclcpp()
node = rclcpp.Node("demo")
pub = node.create_publisher(String, "chatter", 10) # Python msg auto-converts
def on_msg(msg): # msg is the C++ message
print(msg.data)
sub = node.create_subscription(String, "chatter", on_msg, 10)
msg = String(); msg.data = "hi"
pub.publish(msg) # converted to C++ then sent
rclcpp.spin_some(node)
.data directly). The
Python callable is auto-pinned (via cppyy_kit.keep_alive) so it is not collected.
Pattern 2 — tf2 transforms, ingested entirely in C++¶
Use for: looking up transforms without the stock rclpy listener's per-message
Python cost. The C++ tf2_ros::TransformListener ingests /tf on its own thread.
import rclcpp_kit
from rclcpp_kit import tf
rclcpp_kit.bringup_rclcpp()
listener = tf.TransformListener() # own node + own C++ thread
# ... transforms arrive on /tf ...
ts = listener.lookup_transform("world", "sensor", timeout=1.0)
x, y = ts.transform.translation.x, ts.transform.translation.y
ok = listener.can_transform("world", "sensor")
listener.set_transform(a_transform_stamped, is_static=True) # seed directly
time= accepts None (latest) / seconds / an rclpy·rclcpp Time. Missing frames or
a timeout raise tf.TransformException. get_frame_names() returns strs.
Pattern 3 — CDR serialization, byte-compatible with rclpy¶
Use for: wire bytes / bag round-trips.
from rclcpp_kit import serialization as ser
from std_msgs.msg import String
_, Cpp = ser.cpp_message_type_from_python(String)
m = Cpp(); m.data = "payload"
blob = ser.serialized_message_to_bytes(ser.serialize_message(m)) # == rclpy bytes
back = ser.deserialize_message(ser.serialized_message_from_bytes(blob), String)
Pattern 4 — rosbag2 from Python (C++ reader/writer)¶
Use for: reading/writing bags with the C++ rosbag2_cpp stack, or as a
rosbag2_py drop-in.
from rclcpp_kit import rosbag2_cpp
reader = rosbag2_cpp.open_reader("/path/to/bag", storage_id="mcap")
for md in rosbag2_cpp.iter_topics(reader):
print(md.name, md.type)
for sbm in rosbag2_cpp.iter_messages(reader): # C++ SerializedBagMessage
...
from rclcpp_kit import rosbag2_py_compat as rosbag2_py # rosbag2_py-shaped API
Gotchas (the cppyy friction this kit hides, so you know the boundary)¶
- Bringup is a header-parse cost, once.
bringup_rclcpp()JITs the rclcpp headers on the first call; subsequent calls are no-ops. Freeze (PCH) removes the parse, not the per-signature JIT — seedocs/FREEZE.md. tf2_ros::Bufferis deliberately avoided. Its overloaded lookup/canTransform mis-resolve under cppyy and crash; the kit uses the plaintf2::BufferCorewith unambiguouscppdefaccessors. Usetf.TransformListener, not rawtf2_ros::Buffer.- Objects that own C++ threads/executors must be released before shutdown.
tf.TransformListener.close()(also auto-registered) drops the listener beforerclcpp::shutdown(); don't hold one past teardown. - Symbols resolve by soname at call time. If you reach past the kit into another
ROS library,
cppyy_kit.load_libraries([...])it first (see cppyy_kit's SKILL).