Runtime-selected messages
RosLibRust’s usual topic API chooses a generated Rust message type at compile time. The dynamic API is for tools—such as topic inspectors, recorders, bridges, and command-line publishers—that only learn a ROS type name at runtime.
The API has three distinct layers:
DynamicValueis a transport-neutral value tree. It preserves ROS-relevant scalar widths, ordered fields, byte arrays, and non-finite floats, but has no schema by itself.DynamicMessagepairs a value with a generatedMessageDescriptor. Creating one validates and canonicalizes the value through the generated Rust type.DynamicTopicProviderlets a backend advertise or subscribe with aMessageDescriptorchosen at runtime. The backend remains responsible for selecting its wire serializer.
Looking up and validating a message
Code generation emits a MESSAGE_REGISTRY next to the generated modules. It accepts canonical
ROS 1 names and common ROS 2 spellings:
use my_messages::MESSAGE_REGISTRY;
let descriptor = MESSAGE_REGISTRY
.get("std_msgs/String")
.expect("message type was not generated");
let message = descriptor.message_from(&serde_json::json!({
"data": "hello from a runtime-selected type"
}))?;
Every generated message also exposes its descriptor through the RosMessageType trait. When the
concrete type is known, it can be used without a registry lookup:
use my_messages::std_msgs;
use roslibrust::RosMessageType;
let descriptor = &std_msgs::String::DESCRIPTION;
let message = descriptor.message_from(&serde_json::json!({"data": "hello"}))?;
The generated registry is an array of these associated constants; it does not generate separate per-message descriptor definitions.
message_from accepts any Serde Serialize value whose shape represents the selected message.
Structs and string-keyed maps are supported. The registry offers the same operation when the type
name is not resolved separately:
let message = MESSAGE_REGISTRY.message_from(
runtime_type_name,
&serde_json::json!({"data": "hello"}),
)?;
Validation checks required and unknown fields, scalar types and ranges, nested messages, sequence
elements, and fixed array lengths. The stored value is rebuilt in schema field order with the
schema’s exact scalar widths. A raw DynamicValue::Sequence can temporarily contain unlike
elements because it has no element schema; it cannot become a DynamicMessage for a homogeneous
ROS sequence unless every element validates.
Inspecting values
DynamicValue has serde_json::Value-style accessors for walking values without matching every
enum variant:
let message = MESSAGE_REGISTRY.message_from(
"geometry_msgs/Point",
&serde_json::json!({"x": 1.0, "y": 2.0, "z": 3.0}),
)?;
assert!(message.value().is_object());
assert_eq!(message.get("x").and_then(DynamicValue::as_f64), Some(1.0));
Field names and sequence indices can be passed to get:
let frame_id = message
.get("header")
.and_then(|header| header.get("frame_id"))
.and_then(DynamicValue::as_str);
let first = message
.get("values")
.and_then(|values| values.get(0));
The scalar accessors retain ROS type information: as_i16() only succeeds for I16, rather than
silently converting another integer width. is_number(), is_integer(), and related category
checks are available when the exact width is unimportant. Raw DynamicValue also provides
get_mut, as_array_mut, and as_object_mut. DynamicMessage intentionally exposes only
read-only access, preserving the guarantee that its contents satisfy its descriptor.
use roslibrust::{DynamicField, DynamicValue};
let raw = DynamicValue::Message(vec![DynamicField {
name: "data".to_owned(),
value: DynamicValue::Sequence(vec![
DynamicValue::I64(1),
DynamicValue::String("not an integer".to_owned()),
DynamicValue::I64(3),
]),
}]);
// Fails if `data` is an integer sequence in the selected generated type.
let checked = descriptor.message(raw)?;
Dynamic publishing and subscribing
DynamicTopicProvider mirrors TopicProvider, but takes a descriptor instead of a generic Rust
type:
use roslibrust::{DynamicPublish, DynamicSubscribe, DynamicTopicProvider};
let descriptor = MESSAGE_REGISTRY
.get(runtime_type_name)
.expect("message type was not generated");
let mut subscriber = ros.dynamic_subscribe("/chatter", descriptor).await?;
let publisher = ros.dynamic_advertise("/chatter", descriptor).await?;
let message = descriptor.message_from(&serde_json::json!({"data": "hello"}))?;
publisher.publish(&message).await?;
let received = subscriber.next().await?;
println!(
"{}: {}",
received.descriptor().ros_type_name,
serde_json::to_string(received.value())?,
);
For one-off values, publish_serializable combines construction, validation, and publishing:
publisher
.publish_serializable(&serde_json::json!({"data": "hello"}))
.await?;
Construct a DynamicMessage once and use publish when sending the same value repeatedly.
DynamicValue implements serde::Serialize, so tools can select their own display or interchange
format. The selected serializer owns format-specific policies; for example, JSON serializers decide
how to represent byte arrays and non-finite floating-point values.
Serializer ownership
Generated descriptors provide format-neutral Serde conversion operations. They do not choose JSON,
ROS 1 wire encoding, or CDR. A backend implementing DynamicTopicProvider chooses its correct
serializer and deserializer, then calls DynamicMessage::serialize_with or
MessageDescriptor::deserialize_with. This keeps knowledge of transport encoding in the backend
while sharing generated-type lookup and schema validation.
MockRos, native ROS 1, rosbridge, the Zenoh ROS 1 bridge backend, and the Hiroz native ROS 2
backend all implement DynamicTopicProvider. Each implementation selects its native wire format:
bincode for the mock, ROS serialization for ROS 1 and its Zenoh bridge, JSON for rosbridge, and CDR
for Hiroz.