Announcing protoros2: use protobuf in ros2 without compromise

Hi ROS Community! :waving_hand:

For years, there has been a strong and consistent demand for seamless Protobuf serialization in ROS/ROS 2. While there are excellent existing tools in the community, integrating them cleanly into a high-performance, production-ready pipeline often comes with friction.

Today, we’re excited to introduce protoros2 — a middleware wrapper and orchestration engine designed to provide zero-intrusive protobuf support for ROS 2. ZhenshengLee/protoros2: use protobuf in ros2 without compromise

(The name is heavily inspired by the awesome flatros2 GitHub - Ekumen-OS/flatros2 · GitHub project!)

:rocket: What does protoros2 do?

protoros2 does not reinvent the wheel. Instead, it acts as a non-intrusive “Tri-State Orchestration Engine” that elegantly binds third-party Open-Source foundations into a unified architecture. It allows you to use Protobuf in your stack without compromise.

It provides an out-of-the-box EnterpriseNode wrapper that offers multi-channel communication:

• Proto Channel (Zero-Intrusive Fast-Path): Transparently inspects the underlying RMW serialization format at runtime. If the RMW supports Protobuf natively, it routes messages directly via rclcpp::SerializedMessage. If not, it gracefully falls back to standard CDR via rclcpp::TypeAdapter.
• Flat Channel (Performance Bonus): An optional bypass channel optimized for ultra-low latency IPC (powered by Iceoryx shared memory), fully adapted to the ROS 2 executor ecosystem.

:sparkles: Key Features & Use Cases

We designed protoros2 to be flexible enough to accommodate different team workflows, supporting multiple “Single Source of Truth” (SSOT) architectures seamlessly:

• Use Case A: Standard ROS 2 .msg as SSOT
Write your standard .msg files as usual. protoros2 works seamlessly with standard RMWs (CDR only) or native Protobuf RMWs without altering your application logic. In fallback modes, it can even handle simultaneous CDR and Protobuf topic ecosystems flawlessly.
• Use Case B: AI/Robotics .proto as SSOT
For AI-first teams, define your data structures natively in .proto. protoros2 can either co-exist with a generated mirror IDL or operate purely on .proto, bypassing .msg entirely for a direct, zero-overhead binding.
• Native ROS 2 Executor Support:
Whether you prefer standard Callback Push, WaitSets, CallbackGroups (Mutually Exclusive/Reentrant), Polling Subscribers, or Intra-Process Comm—protoros2 natively integrates these paradigms out of the box.
• MLOps Ecosystem Ready:
Full compatibility with mcap format and rosbag2 plugins. Data scientists can consume protobuf bags directly with native python bindings.

:shield: Enterprise Security Built-in

To ensure consistency in large-scale deployments, protoros2 utilizes strict C++ access controls to safely encapsulate the raw rclcpp::Node. It intercepts and disables risky dynamic ROS 2 configurations (like parameter services and QoS overriding) at compile time, guaranteeing predictable behavior on the vehicle edge without sacrificing the standard ROS 2 developer experience.

:folded_hands: Acknowledgement

This work stands on the shoulders of giants. We want to express our deepest gratitude to the following incredible projects and their contributors, without which protoros2 would not have been possible:

• rosidl_typesupport_protobuf https://github.com/eclipse-ecal/rosidl_typesupport_protobuf: For providing the robust C++ TypeSupport handle and TypeAdapter generation engine.
• proto2ros https://github.com/rai-opensource/proto2ros: For the brilliant AST parser bridging .proto definitions to synthetic IDL .msg.
• ros-central-registry https://github.com/intrinsic-opensource/ros-central-registry/blob/main/examples: For the excellent Bazel + ROS 2 integration examples and Protobuf C++ references.

5 Likes

This is neat — the serialization question gets more interesting the moment the data leaves the robot. On-LAN, CDR is fine and the cost is noise. Over a constrained or lossy WAN link (warehouse Wi-Fi, cellular, a VPN back to cloud), wire-size and schema evolution start to matter a lot, and protobuf’s story there is stronger. Is protoros2 aimed mainly at cross-language/interop ergonomics, or also at the wire-efficiency angle for off-robot links? And how are you handling the mismatch with the ROS 2 type system at the boundary?

Thanks for your reply!

you are right about protobuf, we summarize the core advantages of Protobuf in our production pipeline as a four-win scenario: it saves compute and bandwidth resources on the vehicle edge, eliminates massive data conversion times in the cloud, lowers the development friction for our engineers, and collectively drives down overall infrastructure costs.

To answer your first question regarding our aims: It is primarily driven by this off-robot wire-efficiency and MLOps ecosystem integration, though cross-language ergonomics is a nice side-effect.(our current runtime orchestration is heavily focused on C++ for vehicle-edge performance. For Python, we currently only support offline MLOps parsing, e.g., extracting protobufs
directly from .mcap bags, rather than native ROS 2 Python nodes)
While middleware protocols like Zenoh are fantastic for reducing network-level protocol overhead, Protobuf tackles the massive payload overhead. Both are essential for offloading complex tracking arrays or sensor data to the cloud via cellular/Wi-Fi bottlenecks.

we actually tackle this in two dimensions: Compile-Time (Semantic mismatch) and Runtime (Coexistence mismatch).

  1. Compile-Time: Handling Schema & Semantic Mismatch
    We avoid writing any manual conversion code. Instead, we leverage toolchains depending on your chosen “Single Source of Truth” (SSOT):

• If .msg is the SSOT (Use Case A): We rely on rosidl_typesupport_protobuf to map ROS 2 primitives and dynamic arrays into their exact Protobuf equivalents under the hood during the interface generation phase.
• If .proto is the SSOT (Use Case B): This is where concepts like optional or repeated clash with ROS 2. Here, we use the AST parser from proto2ros. At compile-time, it parses the .proto files and synthetically generates a mirrored ROS 2 .msg IDL, automatically applying mapping rules to bridge the semantic gap before generating standard C++ structs.

  1. Runtime Boundary: Coexisting with Legacy Nodes
    You might also wonder: “How does a legacy ROS 2 node using standard .msg and CDR coexist with our new .proto nodes?”

To be clear: A node speaking pure CDR and a node speaking pure Protobuf cannot communicate directly with each other on the same topic. The wire formats are fundamentally different.

However, we handle this ecosystem boundary via Simultaneous Coexistence & Graceful Fallback at the RMW layer. As demonstrated in ucA.3 of our README, our customized RMW (rmw_ecal_proto_cpp) automatically falls back to standard CDR if a message type hasn’t been compiled with Protobuf TypeSupport. This allows you to run legacy CDR topics (e.g., /chatter) and Protobuf topics (e.g., /proto_msg_topic) simultaneously on the same robot, within the same node graph, and even record them into the exact same .mcap bag file seamlessly!

export RMW_IMPLEMENTATION=rmw_ecal_proto_cpp
# 1. rclcpp with prebuilt binary with cdr typesupport only (fallback to cdr)
# example_interface/msg/String.msg has not been compiled with rosidl_typesupport_protobuf, thus dont have protobuf typesupport
ros2 run demo_nodes_cpp talker
ros2 run demo_nodes_cpp listener
# 2. rclcpp with protobuf typesupport
ros2 run protoros2_example example_proto_publisher
ros2 run protoros2_example example_proto_subscriber
# 3. rclpy
ros2 topic list
ros2 topic echo /chatter
ros2 topic echo /proto_msg_topic
# 4. rosbag2 record & info (simultaneous cdr + protobuf topics)
ros2 bag record -o proto_msg_rmw_ecal_both --topics /chatter /proto_msg_topic
ros2 bag info ./proto_msg_rmw_ecal_both/
# 5. bagreader
ros2 run protoros2_example rosbag2_reader.py ./proto_msg_rmw_ecal_both/
# 6. mlops
mcap info ./proto_msg_rmw_ecal_both/0_*.mcap
ros2 run protoros2_example mcap_ros2_reader.py ./proto_msg_rmw_ecal_both/0_*.mcap
ros2 run protoros2_example mcap_proto_reader.py ./proto_msg_rmw_ecal_both/0_*.mcap

Because serialization formats are completely decoupled from application binaries, bridging the boundary requires zero application code changes.

If you simply recompile the underlying .msg package (e.g., std_msgs) with our rosidl_typesupport_protobuf injected and source it in your workspace, your legacy, pre-compiled binary nodes will dynamically dlopen the new Protobuf typesupport at runtime. Under our rmw_ecal_proto_cpp, those legacy nodes instantly upgrade to native Protobuf communication.

export RMW_IMPLEMENTATION=rmw_ecal_proto_cpp
# 1. demo_nodes_cpp is a legacy pre-compiled binary, BUT...
# example_interface/msg/String.msg HAS BEEN compiled with rosidl_typesupport_protobuf
# Thus, the nodes dynamically load Protobuf typesupport at runtime!
ros2 run demo_nodes_cpp talker
ros2 run demo_nodes_cpp listener
# 4. rosbag2 record & info (will be protobuf)
ros2 bag record -o proto_msg_rmw_ecal_both --topics /chatter

PS:
the brilliant foundation for much of this dynamic typesupport magic has actually already been laid out by Google/Intrinsic in the ros-central-registry.
However, their work is heavily integrated into a Bazel ecosystem. What we’ve done with protoros2 is taking that brilliance, porting the heavy lifting into a native CMake/colcon ecosystem, and wrapping it with an out-of-the-box EnterpriseNode orchestration engine (including the Iceoryx FlatChannel bypass) so the broader ROS 2 community can actually deploy it in production right now.

That coexistence story is the part that makes migration realistic: legacy CDR nodes and protobuf nodes sharing one robot and landing in a unified mcap bag. Most teams can’t flag-day their whole graph. The four-win framing matches what I’ve seen on constrained links, where the bandwidth line item gets noticed first but the cloud-side conversion cost is the quiet one. Thanks for the thorough answer. Watching the project.

This looks really cool @ZhenshengLee,

Question: When .proto is the SSOT and proto2ros synthesizes the mirrored .msg IDL, how do you handle the proto schema evolution downstream? If a field gets added or an optional/oneof is introduced upstream, does the regenerated IDL stay compatible with already-running CDR-fallback nodes, or is there a versioning/pinning step in your pipeline to keep the edge and cloud sides in sync? Curious what that workflow looks like in practice.

1 Like

Is the intention to bridge these protobuf messages to devices that do not natively “speak ROS” like embedded systems? That’s something we’ve been working on at Clearpath Robotics. We ended up creating a protobuf-based system of our own called Proton, which bridges protobuf data and ROS 2 messages in a peer-to-peer network

EDIT: on a second reading through your project, I realized that I mistook the purpose of your project. Proton and protoros2 could be complimentary to each other. Proton skips RMW between MCU and PC altogether, and the (unreleased version) proton_ros2 package uses pluginlib to create generic protobuf-based publishers/subscribers using rcl’s serialization API. That could hook into protoros2.

proton_ros2 maps the proton data definitions to ROS 2 message fields (and vice versa) so we don’t end up with .proto versions of every message in the network, but there could be a very easy protobuf translation layer between Bundle.proto to the protoros2 versions of messages. Very interesting.

1 Like

Sorry for the late reply, I just got back from vacation.

we rely on standard protobuf evolution rules: append-only field tags, never reuse/renumber tags, reserved for retired tags. Parsing preserves unknown fields by default in our stack, so a new sender + receiver interoperate without middleware involvement. that’s what we rely on for edge/cloud sync, the cloud side reads the protobuf bytes from MCAP bags.

The answer is no, and in practice we do not rely on CDR for dynamic compatibility, we suggest monorepo build, so the .proto, mirrored .msg, the generated typesupport and all nodes are shipped atomically in one build, old/new cdr nodes never coexist on the same vehicle. The cdr path only exists for interop with stock ROS2 tooling(rviz/echo) and non-protobuf rmws, not as a versioned interop contract.

  1. We maintain a dedicated repository (e.g., project_msgs) for all our .proto definitions. Every stable schema iteration is versioned using Git tags (e.g., v1.2.0).
  2. On the Edge (Vehicle): Strict Versioning & Pinning. When building a vehicle software release (Docker image), the entire build is pinned to a single Git tag. All nodes on that vehicle are compiled against that exact schema state. We never allow mismatched schema versions to run simultaneously on the same vehicle.
  3. On the Cloud: Pure Protobuf & Native Compatibility. Instead of complex metadata parsing to switch Git branches per bag, our cloud data pipelines (and pure Python offline readers) simply use the latest .proto definitions from the master branch.
  4. On the Cloud: There still needs to be version awareness in case of semantic changes to fields.. the version should follow MAJOR.MINOR.PATCH format, any semantic/unit change of an existing field is treated as MAJOR(breaking)

NOTE: This elegant decoupling only works because we strictly enforce Protobuf evolution rules at the CI level for our project_msgs repository. Engineers are strictly forbidden from modifying field tags (numbers) or types, and any deleted fields must be explicitly marked as reserved. As long as we don’t break Protobuf binary rules, the cloud seamlessly ingests all historical edge data without needing strict 1:1 version synchronization.

1 Like

Thanks for your info!

In one word, the proton and proto_ros2 is making PROTO_SSOT in MCU

with protoros2, proto_ros2_node can avoid proto->msg translation and cdr serdes, which could significantly reduce cpu usage.

1 Like

Thanks for creating this, @ZhenshengLee.

It looks like there’s no LICENSE file in the repo so it appears to be closed source. Is that what you intend?

an oss license has been added.

BTW, from ROS PMC Minutes for September 1st, 2026 - ROS / ROS PMC - Open Robotics Discourse

rosbag2#2476: playing bags with mixed serialization formats

Glad to see there are efforts of rosbags for protobuf. Are there any official release plan, like in next LTS version?

1 Like