Skip to main content

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:

  1. MD5 hash of CDR2-encoded TypeObject (16 bytes)
  2. 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

AspectMinimalComplete
PurposeRuntime compatibilityExact matching
NamesIgnoredMust match
AnnotationsSome ignoredAll must match
Use caseWriter→Reader matchingType 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

WriterReaderCompatible?
@final@final (same)Yes
@final@final (different)No
@appendable V1@appendable V2Yes (if V2 adds fields)
@mutable@mutableYes (by member_id)
@final@appendableNo

Implementation Status

FeatureStatus
TypeIdentifierComplete
EquivalenceHash (MD5)Complete
MinimalTypeObjectComplete
CompleteTypeObjectComplete
TypeObjectBuilderComplete
Type Evolution rulesIn progress
SEDP integrationIn progress
Dynamic TypesPost-v1.0

Specification References