XTypes Advanced
OMG DDS-XTypes v1.3 implementation for type evolution and discovery.
Overview
XTypes (Extensible and Dynamic Topic Types) provides:
- TypeIdentifier - Unique type identification (hash-based or direct)
- TypeObject - Runtime type metadata (Minimal and Complete)
- EquivalenceHash - MD5-based type hashing (14 bytes)
- Type Evolution - Schema compatibility rules
Feature Flag
XTypes requires the xtypes feature (enabled by default):
[dependencies]
hdds = { path = "../hdds/crates/hdds", features = ["xtypes"] }
TypeIdentifier
Uniquely identifies a DDS type per XTypes spec section 7.3.4.
Variants
pub enum TypeIdentifier {
// Primitive types (direct identification)
Primitive(TypeKind),
// Bounded strings
StringSmall { bound: u8 }, // string<N> where N <= 255
StringLarge { bound: u32 }, // string<N> where N > 255
WStringSmall { bound: u8 }, // wstring<N> where N <= 255
WStringLarge { bound: u32 }, // wstring<N> where N > 255
// Hash-based (complex types)
Minimal(EquivalenceHash), // Assignability-based
Complete(EquivalenceHash), // Full structural equality
// Cyclic types
StronglyConnected(StronglyConnectedComponentId),
}
Creating TypeIdentifiers
use hdds::xtypes::{TypeIdentifier, TypeKind, EquivalenceHash};
// Primitive types (no hashing)
let int32_id = TypeIdentifier::primitive(TypeKind::TK_INT32);
let float64_id = TypeIdentifier::TK_FLOAT64; // Convenience constant
// Bounded strings
let string_id = TypeIdentifier::string(256); // string<256>
let wstring_id = TypeIdentifier::wstring(128); // wstring<128>
// Complex types (hash-based)
let hash = EquivalenceHash::compute(&type_object_cdr2);
let struct_id = TypeIdentifier::minimal(hash);
let complete_id = TypeIdentifier::complete(hash);
Primitive Type Constants
// Boolean and byte
TypeIdentifier::TK_BOOLEAN
TypeIdentifier::TK_BYTE
// Signed integers
TypeIdentifier::TK_INT8
TypeIdentifier::TK_INT16
TypeIdentifier::TK_INT32
TypeIdentifier::TK_INT64
// Unsigned integers
TypeIdentifier::TK_UINT8
TypeIdentifier::TK_UINT16
TypeIdentifier::TK_UINT32
TypeIdentifier::TK_UINT64
// Floating point
TypeIdentifier::TK_FLOAT32
TypeIdentifier::TK_FLOAT64
// Characters
TypeIdentifier::TK_CHAR8
TypeIdentifier::TK_CHAR16
// Unbounded strings
TypeIdentifier::TK_STRING8
TypeIdentifier::TK_STRING16
TypeIdentifier Methods
let id = TypeIdentifier::minimal(hash);
// Type checks
id.is_primitive(); // false
id.is_string(); // false
id.is_hash_based(); // true
id.is_strongly_connected(); // false
// Get equivalence kind
id.equivalence_kind(); // Some(EquivalenceKind::Minimal)
// Get hash
id.get_hash(); // Some(&EquivalenceHash)
// Get primitive kind
TypeIdentifier::TK_INT32.get_primitive_kind(); // Some(TypeKind::TK_INT32)
EquivalenceHash
14-byte MD5 hash for type identification per XTypes spec section 7.3.4.8.
Computing Hash
use hdds::xtypes::EquivalenceHash;
// Compute from CDR2-encoded TypeObject
let cdr2_bytes = type_object.serialize_cdr2()?;
let hash = EquivalenceHash::compute(&cdr2_bytes);
// Hash is deterministic
let hash2 = EquivalenceHash::compute(&cdr2_bytes);
assert_eq!(hash, hash2);
Hash Operations
// Create from raw bytes
let bytes: [u8; 14] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
let hash = EquivalenceHash::from_bytes(bytes);
// Get raw bytes
let raw: &[u8; 14] = hash.as_bytes();
// Zero hash (placeholder)
let zero = EquivalenceHash::zero();
// Display (hex)
println!("{}", hash); // "0102030405060708090a0b0c0d0e"
Why 14 Bytes?
Per XTypes spec, the EquivalenceHash is:
- MD5 hash of CDR2-encoded TypeObject (16 bytes)
- Truncated to 14 bytes (discard last 2 bytes)
This provides sufficient uniqueness while fitting in RTPS inline QoS.
EquivalenceKind
Determines which equivalence relation to use:
pub enum EquivalenceKind {
Minimal = 0x10, // Assignability-based
Complete = 0x20, // Full structural equality
}
Minimal vs Complete
| Aspect | Minimal | Complete |
|---|---|---|
| Purpose | Runtime compatibility | Exact matching |
| Names | Ignored | Must match |
| Annotations | Some ignored | All must match |
| Use case | Writer→Reader matching | Type equality |
TypeObject
Runtime type metadata in two representations:
MinimalTypeObject
For assignability checking (smaller, faster):
use hdds::xtypes::{MinimalTypeObject, MinimalStructType, MinimalStructMember};
// Minimal representation excludes names and some annotations
let minimal = MinimalTypeObject::Struct(MinimalStructType {
header: MinimalStructHeader { /* ... */ },
members: vec![
MinimalStructMember {
common: CommonStructMember {
member_id: 0,
member_flags: MemberFlag::empty(),
member_type_id: TypeIdentifier::TK_INT32,
},
detail: MinimalMemberDetail { name_hash: 0x12345678 },
},
],
});
CompleteTypeObject
For full type description (includes names, annotations):
use hdds::xtypes::{CompleteTypeObject, CompleteStructType, CompleteStructMember};
// Complete representation includes everything
let complete = CompleteTypeObject::Struct(CompleteStructType {
header: CompleteStructHeader {
detail: CompleteTypeDetail {
type_name: "SensorData".to_string(),
// ...
},
// ...
},
members: vec![
CompleteStructMember {
common: CommonStructMember {
member_id: 0,
member_flags: MemberFlag::empty(),
member_type_id: TypeIdentifier::TK_INT32,
},
detail: CompleteMemberDetail {
name: "sensor_id".to_string(),
// ...
},
},
],
});
TypeObject Compression
use hdds::xtypes::{compress_type_object, decompress_type_object};
// Compress for wire transmission
let compressed = compress_type_object(&type_object_bytes)?;
// Decompress on receive
let decompressed = decompress_type_object(&compressed)?;
Type Evolution
XTypes supports schema evolution with three extensibility modes:
@final
No changes allowed - exact match required:
@final
struct FixedMessage {
long id;
string<64> text;
};
@appendable
Can add members at the end:
@appendable
struct SensorV1 {
@key long id;
float temperature;
};
// V2: Added humidity (compatible!)
@appendable
struct SensorV2 {
@key long id;
float temperature;
@optional float humidity; // New field
};
@mutable
Can add/remove members anywhere (by member_id):
@mutable
struct FlexibleData {
@id(0) @key long id;
@id(1) float value;
@id(2) @optional string label;
};
TypeObjectBuilder
Build TypeObjects at runtime (for ROS 2 introspection):
use hdds::xtypes::{TypeObjectBuilder, MessageDescriptor, MessageMember, PrimitiveType};
let descriptor = MessageDescriptor {
type_name: "sensor_msgs/msg/Temperature".to_string(),
members: vec![
MessageMember {
name: "temperature".to_string(),
field_type: FieldType::Primitive(PrimitiveType::Float64),
is_key: false,
is_optional: false,
},
MessageMember {
name: "variance".to_string(),
field_type: FieldType::Primitive(PrimitiveType::Float64),
is_key: false,
is_optional: false,
},
],
};
let builder = TypeObjectBuilder::new();
let (minimal, complete) = builder.build(&descriptor)?;
StronglyConnectedComponentId
For types with cyclic dependencies:
pub struct StronglyConnectedComponentId {
pub sc_component_id: EquivalenceHash, // Hash of the component
pub scc_length: i32, // Types in component
pub scc_index: i32, // Index within component
}
Example cyclic type:
struct Node {
long value;
sequence<Node> children; // Self-reference
};
Type Compatibility Matrix
| Writer | Reader | Compatible? |
|---|---|---|
@final | @final (same) | Yes |
@final | @final (different) | No |
@appendable V1 | @appendable V2 | Yes (if V2 adds fields) |
@mutable | @mutable | Yes (by member_id) |
@final | @appendable | No |
Implementation Status
| Feature | Status |
|---|---|
| TypeIdentifier | Complete |
| EquivalenceHash (MD5) | Complete |
| MinimalTypeObject | Complete |
| CompleteTypeObject | Complete |
| TypeObjectBuilder | Complete |
| Type Evolution rules | In progress |
| SEDP integration | In progress |
| Dynamic Types | Post-v1.0 |
Specification References
- OMG DDS-XTypes v1.3: https://www.omg.org/spec/DDS-XTypes/1.3/
- TypeObject IDL: https://www.omg.org/spec/DDS-XTypes/20190301/dds-xtypes_typeobject.idl
- DDS v1.4: Section 2.2.3 (Type Representation)
Related
- Dynamic Types - Runtime type construction
- CDR2 Overview - Wire format
- Interoperability - Cross-vendor compatibility