Skip to main content

XTypes (Extensible Types)

XTypes enables type evolution and compatibility between different versions of data types.

Overview

XTypes (DDS-XTypes v1.3) provides:

  • Type evolution - Add/remove fields without breaking compatibility
  • Type compatibility - Rules for matching different type versions
  • Dynamic types - Runtime type discovery and introspection
  • Type objects - Machine-readable type descriptions

Extensibility Kinds

Final Types

Cannot be extended. Strictest compatibility.

@final
struct SensorReading {
uint32 sensor_id;
float value;
};
  • Readers and writers must have identical types
  • Smallest wire size
  • Best performance

Appendable Types (Default)

New fields can be added at the end.

@appendable
struct SensorReading {
uint32 sensor_id;
float value;
// Future: can add fields here
};
  • New readers can read old data (new fields get defaults)
  • Old readers can read new data (ignore extra fields)
  • Moderate wire overhead (DHEADER)

Mutable Types

Fields can be added, removed, or reordered.

@mutable
struct SensorReading {
@id(1) uint32 sensor_id;
@id(2) float value;
@id(3) @optional string unit;
};
  • Maximum flexibility
  • Higher wire overhead (EMHEADER per member)
  • Requires @id annotations

Member Annotations

@id - Member Identity

@mutable
struct Config {
@id(1) uint32 version;
@id(2) string name;
@id(3) float threshold; // Can reorder, ID preserved
};

@optional - Optional Fields

struct SensorData {
uint32 sensor_id;
float value;
@optional float uncertainty; // May be absent
@optional string notes;
};

Reading optional fields:

let sample = reader.take_one()?;
if let Some(uncertainty) = sample.uncertainty {
println!("Uncertainty: {}", uncertainty);
}

@default - Default Values

struct Config {
uint32 version;
@default(100) uint32 timeout_ms;
@default("unnamed") string name;
};

@must_understand

Fields that receivers must support:

@mutable
struct Command {
@id(1) @must_understand uint32 command_id;
@id(2) string parameters;
};

Readers that don't recognize @must_understand fields reject the sample.

Type Evolution Examples

Adding a Field (Appendable)

Version 1:

@appendable
struct SensorV1 {
uint32 sensor_id;
float value;
};

Version 2:

@appendable
struct SensorV2 {
uint32 sensor_id;
float value;
uint64 timestamp; // Added field
};
WriterReaderResult
V1V1Works
V2V2Works
V1V2Works (timestamp = default)
V2V1Works (timestamp ignored)

Reordering Fields (Mutable)

Version 1:

@mutable
struct ConfigV1 {
@id(1) string name;
@id(2) uint32 value;
};

Version 2:

@mutable
struct ConfigV2 {
@id(2) uint32 value; // Reordered
@id(1) string name;
@id(3) float scale; // Added
};

Fields are matched by @id, not position.

Adding Optional Fields

// Original
struct Robot {
uint32 robot_id;
float position_x;
float position_y;
};

// Extended
struct Robot {
uint32 robot_id;
float position_x;
float position_y;
@optional float position_z; // New
@optional float orientation; // New
};

Type Compatibility

Type Consistency Enforcement

// Strict: types must be identical
let qos = DataReaderQos::default()
.type_consistency(TypeConsistency::DisallowTypeCoercion);

// Allow compatible types (default)
let qos = DataReaderQos::default()
.type_consistency(TypeConsistency::AllowTypeCoercion);

// Ignore member names, match by structure
let qos = DataReaderQos::default()
.type_consistency(TypeConsistency::IgnoreMemberNames);

Compatibility Rules

Always Compatible:

  • Same type definition
  • Appendable types with added trailing fields (if optional/default)

Compatible with AllowTypeCoercion:

  • Mutable types with different field order
  • Types with optional fields added/removed
  • Widening conversions (int16 -> int32)

Never Compatible:

  • Final types with any difference
  • Changed field types (incompatible)
  • Required field removed

Type Objects

XTypes uses TypeObjects for runtime type information:

// Get type object for a registered type
let type_object = participant.get_type_object::<SensorData>()?;

println!("Type name: {}", type_object.name());
println!("Extensibility: {:?}", type_object.extensibility());

for member in type_object.members() {
println!(" {} (id={}): {:?}",
member.name(),
member.id(),
member.type_kind()
);
}

Type Identifier

Types are identified by hash:

let type_id = TypeIdentifier::from_type::<SensorData>();
println!("Type ID: {:?}", type_id); // 14-byte hash

Type Discovery

Discover types from remote participants:

for topic in participant.discovered_topics() {
if let Some(type_info) = topic.type_info {
println!("Topic: {}", topic.name);
println!(" Type: {}", type_info.type_name);
println!(" Type ID: {:?}", type_info.type_id);
}
}

Dynamic Types

Create types at runtime:

use hdds::dynamic::*;

// Build type dynamically
let sensor_type = DynamicTypeBuilder::new("SensorData")
.extensibility(Extensibility::Appendable)
.add_member("sensor_id", TypeKind::UInt32)
.add_member("value", TypeKind::Float32)
.add_optional_member("timestamp", TypeKind::UInt64)
.build()?;

// Create dynamic data
let mut data = DynamicData::new(&sensor_type);
data.set_u32("sensor_id", 42)?;
data.set_f32("value", 23.5)?;

// Write dynamic data
let writer = publisher.create_dynamic_writer("SensorTopic", &sensor_type)?;
writer.write(&data)?;

IDL Annotations Summary

AnnotationApplies ToPurpose
@finalStructNo extension allowed
@appendableStructAdd fields at end
@mutableStructFull flexibility
@id(N)MemberStable member identity
@optionalMemberMay be absent
@default(V)MemberDefault value
@must_understandMemberRequired for receivers
@keyMemberInstance key
@externalMemberSeparate allocation

Best Practices

  1. Start with @appendable - Good balance of flexibility and efficiency
  2. Use @id on mutable types - Enables safe reordering
  3. Make new fields @optional - Backward compatible
  4. Use @default for required fields - Forward compatible
  5. Avoid @final unless needed - Limits evolution

Migration Strategy

Phase 1: Plan

// Add version field for explicit versioning
@appendable
struct SensorData {
uint32 schema_version; // Track schema changes
uint32 sensor_id;
float value;
};

Phase 2: Add Fields

// Add optional fields (backward compatible)
@appendable
struct SensorData {
uint32 schema_version;
uint32 sensor_id;
float value;
@optional uint64 timestamp; // New in v2
};

Phase 3: Deprecate

// Mark old fields (still present for compatibility)
@appendable
struct SensorData {
uint32 schema_version;
@deprecated uint32 sensor_id; // Use sensor_guid instead
float value;
@optional uint64 timestamp;
@optional string sensor_guid; // Replacement
};

Troubleshooting

Type Mismatch Error

Error: TypeConsistency check failed
  • Check extensibility annotations match
  • Verify @id values are consistent
  • Check type names match exactly

Missing Optional Field

// Handle gracefully
let value = sample.optional_field.unwrap_or_default();

Unknown Member ID

For mutable types with unknown members:

let qos = DataReaderQos::default()
.type_consistency(TypeConsistency::AllowTypeCoercion)
.ignore_unknown_members(true);

Next Steps