Skip to main content

Dynamic Types

Runtime type manipulation for DDS without compile-time type knowledge.

Overview

Dynamic Types enable building and manipulating DDS types at runtime, without needing compile-time type definitions. This is useful for:

  • Generic tools - DDS monitors, bridges, routers
  • Type introspection - Inspecting received data structure
  • Protocol adapters - Converting between formats
  • Testing - Creating arbitrary test data

Feature Flag

Dynamic Types requires the dynamic-types feature:

[dependencies]
hdds = { path = "../hdds/crates/hdds", features = ["dynamic-types"] }

Quick Start

use hdds::dynamic::{TypeDescriptorBuilder, DynamicData, PrimitiveKind};

// Build a type descriptor at runtime
let descriptor = TypeDescriptorBuilder::new("SensorReading")
.field("sensor_id", PrimitiveKind::U32)
.field("temperature", PrimitiveKind::F64)
.field("timestamp", PrimitiveKind::U64)
.build();

// Create dynamic data
let desc = std::sync::Arc::new(descriptor);
let mut data = DynamicData::new(&desc);

// Set field values
data.set("sensor_id", 42u32).unwrap();
data.set("temperature", 23.5f64).unwrap();
data.set("timestamp", 1702900000u64).unwrap();

// Read field values
let temp: f64 = data.get("temperature").unwrap();
assert_eq!(temp, 23.5);

Type Descriptors

Primitive Types

use hdds::dynamic::PrimitiveKind;

// Integer types
PrimitiveKind::Bool // bool
PrimitiveKind::U8 // uint8
PrimitiveKind::U16 // uint16
PrimitiveKind::U32 // uint32
PrimitiveKind::U64 // uint64
PrimitiveKind::I8 // int8
PrimitiveKind::I16 // int16
PrimitiveKind::I32 // int32
PrimitiveKind::I64 // int64

// Floating point
PrimitiveKind::F32 // float
PrimitiveKind::F64 // double
PrimitiveKind::LongDouble // long double (platform-dependent)

// Character types
PrimitiveKind::Char // char (1 byte)

// String types
PrimitiveKind::String { max_length: None } // unbounded string
PrimitiveKind::String { max_length: Some(100) } // bounded string<100>
PrimitiveKind::WString { max_length: None } // unbounded wstring
PrimitiveKind::WString { max_length: Some(50) } // bounded wstring<50>

Building Struct Types

use hdds::dynamic::{TypeDescriptorBuilder, PrimitiveKind};
use std::sync::Arc;

let descriptor = TypeDescriptorBuilder::new("Point3D")
.field("x", PrimitiveKind::F64)
.field("y", PrimitiveKind::F64)
.field("z", PrimitiveKind::F64)
.build();

// With strings and sequences
let packet = TypeDescriptorBuilder::new("DataPacket")
.field("id", PrimitiveKind::U32)
.string_field("label")
.bounded_string_field("code", 8) // string<8>
.sequence_field("data", PrimitiveKind::U8)
.bounded_sequence_field("samples", PrimitiveKind::F32, 100)
.build();

Building Arrays

let matrix = TypeDescriptorBuilder::new("Matrix3x3")
.array_field("values", PrimitiveKind::F64, 9)
.build();

Building Nested Structs

let point = Arc::new(
TypeDescriptorBuilder::new("Point")
.field("x", PrimitiveKind::F64)
.field("y", PrimitiveKind::F64)
.build()
);

let rectangle = TypeDescriptorBuilder::new("Rectangle")
.nested_field("top_left", point.clone())
.nested_field("bottom_right", point)
.field("area", PrimitiveKind::F64)
.build();

Building Enums

use hdds::dynamic::EnumBuilder;

// Auto-incrementing values (0, 1, 2)
let color = EnumBuilder::new("Color")
.variant("RED")
.variant("GREEN")
.variant("BLUE")
.build();

// Explicit values
let http_status = EnumBuilder::new("HttpStatus")
.variant_value("OK", 200)
.variant_value("NOT_FOUND", 404)
.variant_value("SERVER_ERROR", 500)
.build();

Building Unions

use hdds::dynamic::UnionBuilder;

let value = UnionBuilder::with_u32_discriminator("Value")
.primitive_case("int_val", 0, PrimitiveKind::I32)
.primitive_case("float_val", 1, PrimitiveKind::F64)
.primitive_case("str_val", 2, PrimitiveKind::String { max_length: None })
.build();

DynamicData API

Creating and Setting Values

use hdds::dynamic::{DynamicData, TypeDescriptorBuilder, PrimitiveKind};
use std::sync::Arc;

let desc = Arc::new(
TypeDescriptorBuilder::new("Message")
.field("id", PrimitiveKind::U32)
.field("payload", PrimitiveKind::String { max_length: None })
.build()
);

let mut data = DynamicData::new(&desc);

// Set primitive fields
data.set("id", 123u32)?;
data.set("payload", "Hello, DDS!")?;

Getting Values

// Typed get
let id: u32 = data.get("id")?;
let payload: String = data.get("payload")?;

// Raw field access
let field_value = data.get_field("id")?;
let field_mut = data.get_field_mut("payload")?;

Working with Sequences

use hdds::dynamic::DynamicValue;

let desc = Arc::new(
TypeDescriptorBuilder::new("DataArray")
.sequence_field("values", PrimitiveKind::I32)
.build()
);

let mut data = DynamicData::new(&desc);

// Push elements
data.push_element(DynamicValue::I32(10))?;
data.push_element(DynamicValue::I32(20))?;
data.push_element(DynamicValue::I32(30))?;

// Get element by index
let elem = data.get_element(1)?; // 20

// Set element by index
data.set_element(0, DynamicValue::I32(100))?;

// Get length
let len = data.len()?; // 3

// Iterate
for elem in data.elements() {
println!("{:?}", elem);
}

Working with Arrays

let desc = Arc::new(
TypeDescriptorBuilder::new("Vector3")
.array_field("coords", PrimitiveKind::F32, 3)
.build()
);

let mut data = DynamicData::new(&desc);

// Arrays are pre-initialized with default values
data.set_element(0, DynamicValue::F32(1.0))?;
data.set_element(1, DynamicValue::F32(2.0))?;
data.set_element(2, DynamicValue::F32(3.0))?;

Iterating Over Fields

for (name, value) in data.fields() {
println!("{}: {:?}", name, value);
}

DynamicValue Enum

The DynamicValue enum represents any DDS value:

pub enum DynamicValue {
// Primitives
Bool(bool),
U8(u8), U16(u16), U32(u32), U64(u64),
I8(i8), I16(i16), I32(i32), I64(i64),
F32(f32), F64(f64),
LongDouble([u8; LONG_DOUBLE_SIZE]),
Char(char),
String(String),
WString(String),

// Complex types
Struct(HashMap<String, DynamicValue>),
Sequence(Vec<DynamicValue>),
Array(Vec<DynamicValue>),
Enum(i64, String), // (value, variant_name)
Union(i64, String, Box<DynamicValue>), // (discriminator, case_name, value)

// Special
Null,
}

CDR Encoding/Decoding

Dynamic data can be encoded to/decoded from CDR wire format:

use hdds::dynamic::{encode_dynamic, decode_dynamic, DynamicData, TypeDescriptorBuilder, PrimitiveKind};
use std::sync::Arc;

let desc = Arc::new(
TypeDescriptorBuilder::new("SensorData")
.field("sensor_id", PrimitiveKind::U32)
.field("value", PrimitiveKind::F64)
.string_field("unit")
.build()
);

// Create and populate data
let mut data = DynamicData::new(&desc);
data.set("sensor_id", 42u32)?;
data.set("value", 23.5f64)?;
data.set("unit", "celsius")?;

// Encode to CDR bytes
let bytes: Vec<u8> = encode_dynamic(&data)?;

// Decode from CDR bytes
let decoded = decode_dynamic(&bytes, &desc)?;

assert_eq!(decoded.get::<u32>("sensor_id")?, 42);

CDR Encoding Rules

TypeAlignmentSize
bool, u8, i8, char11 byte
u16, i1622 bytes
u32, i32, f3244 bytes
u64, i64, f6488 bytes
string44 (length) + N + 1 (null)
sequence<T>44 (length) + N * sizeof(T)
T[N]align(T)N * sizeof(T)

Error Handling

use hdds::dynamic::{DynamicDataError, DynamicCdrError};

// DynamicData errors
match data.get::<u32>("nonexistent") {
Err(DynamicDataError::FieldNotFound(name)) => {
println!("Field not found: {}", name);
}
Err(DynamicDataError::TypeMismatch { expected, got }) => {
println!("Type mismatch: expected {}, got {}", expected, got);
}
Err(DynamicDataError::IndexOutOfBounds { index, length }) => {
println!("Index {} out of bounds (length {})", index, length);
}
Err(DynamicDataError::SequenceTooLong { length, max }) => {
println!("Sequence too long: {} > {}", length, max);
}
_ => {}
}

// CDR errors
match decode_dynamic(&bytes, &desc) {
Err(DynamicCdrError::BufferTooSmall { need, have }) => {
println!("Buffer too small: need {}, have {}", need, have);
}
Err(DynamicCdrError::TypeMismatch { expected, found }) => {
println!("Type mismatch: expected {}, found {}", expected, found);
}
_ => {}
}

Type Descriptor Methods

// Check type kind
descriptor.is_primitive();
descriptor.is_struct();

// Get struct fields
if let Some(fields) = descriptor.fields() {
for field in fields {
println!("{}: {:?}", field.name, field.type_desc.kind);
}
}

// Get field by name
let field = descriptor.field("sensor_id");
let index = descriptor.field_index("sensor_id");

// Get size and alignment info
let min_size = descriptor.min_size();
let alignment = descriptor.alignment();

Example: Generic DDS Bridge

use hdds::dynamic::{TypeDescriptorBuilder, DynamicData, encode_dynamic, decode_dynamic, PrimitiveKind};
use std::sync::Arc;

/// Bridge that transforms sensor data between formats
fn transform_sensor_data(
input: &[u8],
input_desc: &Arc<TypeDescriptor>,
output_desc: &Arc<TypeDescriptor>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
// Decode input
let input_data = decode_dynamic(input, input_desc)?;

// Create output
let mut output_data = DynamicData::new(output_desc);

// Transform fields (example: copy common fields)
if let Ok(id) = input_data.get::<u32>("id") {
output_data.set("sensor_id", id)?;
}
if let Ok(value) = input_data.get::<f64>("reading") {
output_data.set("value", value)?;
}

// Encode output
Ok(encode_dynamic(&output_data)?)
}