Skip to main content

zerodds_dcps/
dds_type.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `DdsType` — der Trait, den User-Typen erfuellen muessen, um ueber
4//! DDS verschickt zu werden.
5//!
6//! # Usage
7//!
8//! User-Typen erfuellen den Trait entweder per Hand oder ueber
9//! die Codegen-Pipeline `zerodds-idl-rust` (IDL → Rust mit
10//! abgeleitetem `DdsType`-Impl). Die Encoder-/Decoder-Paerchen
11//! folgen XCDR2-Konvention (siehe `zerodds-cdr`); der Trait bleibt
12//! transport- und qos-agnostisch.
13//!
14//! # Interop-Hinweis
15//!
16//! Fuer Interop mit Cyclone/Fast-DDS MUSS der `TYPE_NAME` exakt mit
17//! dem Remote-Topic-Typnamen uebereinstimmen (strict equality). Das
18//! IDL-Type-Namespacing (z.B. `std_msgs::msg::String`) muss
19//! beruecksichtigt werden.
20
21extern crate alloc;
22use alloc::vec::Vec;
23
24pub use zerodds_cdr::{KEY_HASH_LEN, PlainCdr2BeKeyHolder, compute_key_hash};
25
26/// XTypes 1.3 §7.4.5 Struct-Extensibility-Kind. Wire-relevante
27/// Information fuer den Sample-Encoder; gleicht den IDL-Annotationen
28/// `@final` / `@appendable` / `@mutable`.
29///
30/// Spec: `zerodds-xcdr2-rust` §2 referenziert das als
31/// `ExtensibilityKind`; der Implementations-Name `Extensibility` und
32/// der spec-aligned Alias [`ExtensibilityKind`] sind identisch.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[repr(u8)]
35pub enum Extensibility {
36    /// `@final`: tight-packed body, kein Header.
37    Final = 0,
38    /// `@appendable`: 4-byte DHEADER + body, forward-compatible.
39    Appendable = 1,
40    /// `@mutable`: pro Member ein EMHEADER + Body.
41    Mutable = 2,
42}
43
44/// Spec-aligned Alias: `zerodds-xcdr2-rust` §2 referenziert die
45/// Extensibility-Enum unter dem Namen `ExtensibilityKind`. Wir
46/// behalten `Extensibility` als Implementations-Name; beide sind via
47/// Alias identisch.
48pub type ExtensibilityKind = Extensibility;
49
50/// Typ, der via DDS published/subscribed werden kann.
51pub trait DdsType: Sized {
52    /// Vollqualifizierter Topic-Type-Name (z.B. `"std_msgs::String"`).
53    /// Muss exakt zum Peer-Type-Namen passen (strict matching).
54    const TYPE_NAME: &'static str;
55
56    /// XTypes 1.3 §7.4.5 Struct-Extensibility-Kind. Default `Final`
57    /// fuer Backwards-Kompat zu pre-`EXTENSIBILITY`-Codegen-Outputs.
58    /// Spec: zerodds-xcdr2-rust §2.3.
59    const EXTENSIBILITY: Extensibility = Extensibility::Final;
60
61    /// `true` wenn der Topic-Type **keyed** ist (mindestens ein Member
62    /// mit `@key`-Annotation). Default `false` — Caller (proc-macro)
63    /// ueberschreibt fuer keyed Types und implementiert auch
64    /// [`Self::encode_key_holder_be`].
65    ///
66    /// Spec: XTypes 1.3 §7.6.8 (KeyHash-Pflicht fuer keyed Topics).
67    ///
68    /// Hinweis (`zerodds-xcdr2-rust` §11 Errata): die Spec referenziert
69    /// dieses Feld als `IS_KEYED`. Wir behalten `HAS_KEY` fuer
70    /// Source-Kompat zu Pre-1.0 Code; der spec-aligned Alias
71    /// [`Self::IS_KEYED`] gibt jederzeit denselben Wert.
72    const HAS_KEY: bool = false;
73
74    /// Spec-aligned Alias fuer [`Self::HAS_KEY`].
75    /// `zerodds-xcdr2-rust` §2 referenziert das als `IS_KEYED`.
76    const IS_KEYED: bool = Self::HAS_KEY;
77
78    /// Maximale Groesse des PLAIN_CDR2-BE-KeyHolder-Streams in Bytes
79    /// (XTypes 1.3 §7.6.8.4 Step 5). `None` = nicht keyed oder
80    /// unbounded (MD5-Pfad). `Some(n)` mit `n <= 16` = zero-pad-Pfad.
81    const KEY_HOLDER_MAX_SIZE: Option<usize> = None;
82
83    /// `true` wenn der Type mit `@nested` annotiert ist (XTypes 1.3
84    /// §7.4.6.3.5). Nested-Types sind nur als Member anderer Types
85    /// gedacht und MUESSEN nicht als DDS-Topic-Type registriert
86    /// werden. `DomainParticipant::create_topic` lehnt registration
87    /// von nested-Types mit `PreconditionNotMet` ab.
88    const IS_NESTED: bool = false;
89
90    /// XTypes 1.3 §7.3.4.2 — TypeIdentifier des Types fuer XTypes-aware
91    /// Discovery + Compatibility-Matching. Default `TypeIdentifier::None`
92    /// signalisiert "type-id nicht bereitgestellt; Reader-Writer-Match
93    /// faellt zurueck auf reinen `type_name`-Vergleich (DDS 1.4 §2.2.3
94    /// Default-Path)".
95    ///
96    /// idl-rust Codegen emittiert hier den passenden TypeIdentifier:
97    /// - Primitive `int32` → `TypeIdentifier::Primitive(PrimitiveKind::Int32)`,
98    /// - String `string<N>` → `TypeIdentifier::String8Small{ bound }`,
99    /// - Composite struct → `TypeIdentifier::EquivalenceHash` (sobald
100    ///   die TypeRegistry-Lookup live ist).
101    ///
102    /// Sobald beide Seiten (Writer + Reader) einen TypeIdentifier
103    /// liefern, ruft der Subscriber-Match-Pfad
104    /// [`zerodds_types::type_matcher::TypeMatcher::match_types`] auf
105    /// (XTypes §7.6.3.7 + DDS 1.4 §2.2.3 TypeConsistencyEnforcement).
106    const TYPE_IDENTIFIER: zerodds_types::TypeIdentifier = zerodds_types::TypeIdentifier::None;
107
108    /// Serialisiert `self` in den XCDR2-Payload, der in einer
109    /// DATA-Submessage als `serialized_payload` gesendet wird.
110    /// Default-Endianness: Little-Endian (RTPS 2.5 §10.5
111    /// `RepresentationIdentifier = CDR2_LE = 0x0010`).
112    ///
113    /// # Errors
114    /// CDR-Encoder-Fehler (Buffer-Overflow etc.).
115    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError>;
116
117    /// Big-Endian-Variante von [`Self::encode`]. Default-Implementation
118    /// delegiert auf [`Self::encode`] (kein Byte-Swap), da generischer
119    /// BE-Re-Encode ohne Type-Reflection nicht moeglich ist. Codegen
120    /// ueberschreibt das fuer Strukturen, die echt BE auf die Wire
121    /// gehen sollen. Spec: zerodds-xcdr2-rust §2.4.
122    ///
123    /// # Errors
124    /// CDR-Encoder-Fehler.
125    fn encode_be(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
126        self.encode(out)
127    }
128
129    /// Deserialisiert einen XCDR2-Payload. Der Caller stellt sicher,
130    /// dass `bytes` den vollen Sample-Payload enthaelt.
131    ///
132    /// # Errors
133    /// CDR-Decoder-Fehler (Truncation, unerwartete Bytes, etc.).
134    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError>;
135
136    /// Serialisiert die `@key`-Member-Werte im **PLAIN_CDR2-BE**-Format
137    /// in den uebergebenen [`PlainCdr2BeKeyHolder`]. Reihenfolge: nach
138    /// `member_id` aufsteigend (XTypes 1.3 §7.6.8.3.1.b).
139    ///
140    /// **Default-Implementation**: leerer Schreibvorgang. Keyed Types
141    /// MUESSEN das ueberschreiben.
142    ///
143    /// Wird vom DcpsRuntime im Sample-Encode-Pfad aufgerufen, um
144    /// PID_KEY_HASH in die Inline-QoS zu schreiben.
145    fn encode_key_holder_be(&self, _holder: &mut PlainCdr2BeKeyHolder) {
146        // Default: kein Key. Keyed Types ueberschreiben.
147    }
148
149    /// Liefert den Wert eines Feldpfads (dotted, z.B. `"a.b"`) als
150    /// `zerodds_sql_filter::Value` fuer SQL-Filter-Evaluation in
151    /// QueryCondition / ContentFilteredTopic. Default: `None` (kein
152    /// Feld erreichbar — der Filter denied dann jedes Sample, sofern
153    /// es einen Feldzugriff enthaelt).
154    ///
155    /// Spec: DDS 1.4 §B.2.1 (Filter Expressions) iVm. §2.2.2.5.9
156    /// (QueryCondition) und §2.2.2.3.5 (ContentFilteredTopic).
157    /// Generierte IDL-Stubs ueberschreiben das per Field.
158    #[must_use]
159    fn field_value(&self, _path: &str) -> Option<zerodds_sql_filter::Value> {
160        None
161    }
162
163    /// Berechnet den 16-Byte KeyHash dieser Instanz nach XTypes 1.3
164    /// §7.6.8.4. `None` wenn `HAS_KEY = false`.
165    ///
166    /// Default-Implementation nutzt [`Self::encode_key_holder_be`] +
167    /// [`Self::KEY_HOLDER_MAX_SIZE`] und delegiert an
168    /// [`compute_key_hash`].
169    #[must_use]
170    fn compute_key_hash(&self) -> Option<[u8; KEY_HASH_LEN]> {
171        if !Self::HAS_KEY {
172            return None;
173        }
174        let mut holder = PlainCdr2BeKeyHolder::new();
175        self.encode_key_holder_be(&mut holder);
176        let max = Self::KEY_HOLDER_MAX_SIZE.unwrap_or(usize::MAX);
177        Some(compute_key_hash(holder.as_bytes(), max))
178    }
179
180    /// Spec-aligned Alias fuer [`Self::compute_key_hash`].
181    /// `zerodds-xcdr2-rust` §2.5 nutzt den Namen `key_hash`; der
182    /// Implementations-Name behaelt `compute_key_hash` aus
183    /// historischer Kompat. Beide liefern denselben Wert.
184    #[must_use]
185    fn key_hash(&self) -> Option<[u8; KEY_HASH_LEN]> {
186        self.compute_key_hash()
187    }
188}
189
190/// `RowAccess`-Adapter fuer einen `DdsType`-Sample-Wert. Wird vom
191/// DataReader in `read_w_condition`/`take_w_condition` und vom
192/// `ContentFilteredTopic`-Filter benutzt.
193pub struct DdsTypeRow<'a, T: DdsType> {
194    /// Inneres Sample, dessen Felder per [`DdsType::field_value`]
195    /// abgefragt werden.
196    pub sample: &'a T,
197}
198
199impl<'a, T: DdsType> DdsTypeRow<'a, T> {
200    /// Konstruktor.
201    #[must_use]
202    pub fn new(sample: &'a T) -> Self {
203        Self { sample }
204    }
205}
206
207impl<T: DdsType> zerodds_sql_filter::RowAccess for DdsTypeRow<'_, T> {
208    fn get(&self, path: &str) -> Option<zerodds_sql_filter::Value> {
209        self.sample.field_value(path)
210    }
211}
212
213/// Platzhalter-Error fuer DdsType::encode. In v1.3 wird das auf
214/// `zerodds_cdr::EncodeError` re-exported, sobald wir den CDR-Layer
215/// in DCPS-Sicht stabilisiert haben.
216#[derive(Debug, Clone, PartialEq, Eq)]
217#[non_exhaustive]
218pub enum EncodeError {
219    /// Buffer-Overflow oder feldspezifischer Wertebereichs-Fehler.
220    Invalid {
221        /// Statische Beschreibung.
222        what: &'static str,
223    },
224}
225
226impl core::fmt::Display for EncodeError {
227    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
228        match self {
229            Self::Invalid { what } => write!(f, "encode error: {what}"),
230        }
231    }
232}
233
234#[cfg(feature = "std")]
235impl std::error::Error for EncodeError {}
236
237impl From<zerodds_cdr::EncodeError> for EncodeError {
238    fn from(e: zerodds_cdr::EncodeError) -> Self {
239        // zerodds-cdr-Fehler werden als opaker `Invalid`-Wrap weitergegeben.
240        // Das ist ausreichend fuer DdsType-Caller, die nur „encoding hat
241        // nicht geklappt"-Information brauchen — die detaillierte
242        // Fehlerstruktur lebt im cdr-Layer und wird via Display
243        // serialisiert wenn ein Caller die Fehlermeldung loggt.
244        let _ = e;
245        Self::Invalid {
246            what: "zerodds_cdr encode error",
247        }
248    }
249}
250
251/// Platzhalter-Error fuer DdsType::decode.
252#[derive(Debug, Clone, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum DecodeError {
255    /// Truncation oder Wertebereich out-of-range.
256    Invalid {
257        /// Statische Beschreibung.
258        what: &'static str,
259    },
260}
261
262impl core::fmt::Display for DecodeError {
263    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
264        match self {
265            Self::Invalid { what } => write!(f, "decode error: {what}"),
266        }
267    }
268}
269
270#[cfg(feature = "std")]
271impl std::error::Error for DecodeError {}
272
273impl From<zerodds_cdr::DecodeError> for DecodeError {
274    fn from(e: zerodds_cdr::DecodeError) -> Self {
275        let _ = e;
276        Self::Invalid {
277            what: "zerodds_cdr decode error",
278        }
279    }
280}
281
282// ---------------------------------------------------------------------
283// DdsAny — IDL `any` Type-Erasure (XCDR2 §7.4.4.7)
284//
285// Wire-Format: TypeIdentifier-Header (CDR-String) + Payload-Bytes.
286// Pure-Rust ohne externe Crate-Dep; Volle Type-Erasure via String-Tag.
287
288/// IDL-`any` als Type-Erasure-Wrapper. Traegt einen Type-Identifier-
289/// String (z.B. `"std_msgs::Header"`) plus die Payload-Bytes.
290///
291/// Konsumenten-Pattern: man prueft `type_name`, deserialisiert die
292/// `payload` mit dem konkreten DdsType.
293#[derive(Debug, Clone, PartialEq, Eq, Default)]
294pub struct DdsAny {
295    /// Voll-qualifizierter Type-Name (entspricht `DdsType::TYPE_NAME`).
296    pub type_name: alloc::string::String,
297    /// XCDR2-Payload-Bytes des wraped Werts.
298    pub payload: Vec<u8>,
299}
300
301impl DdsAny {
302    /// Konstruiert ein `DdsAny` aus einem `DdsType`-Wert.
303    ///
304    /// # Errors
305    /// `EncodeError` bei Encode-Fehler.
306    pub fn pack<T: DdsType>(value: &T) -> Result<Self, EncodeError> {
307        let mut payload = Vec::new();
308        value.encode(&mut payload)?;
309        Ok(Self {
310            type_name: alloc::string::String::from(T::TYPE_NAME),
311            payload,
312        })
313    }
314
315    /// Versucht das Wrapped als `T` zu entpacken.
316    ///
317    /// # Errors
318    /// `DecodeError::Invalid` wenn `T::TYPE_NAME != self.type_name`
319    /// oder Decode-Fehler.
320    pub fn unpack<T: DdsType>(&self) -> Result<T, DecodeError> {
321        if self.type_name != T::TYPE_NAME {
322            return Err(DecodeError::Invalid {
323                what: "DdsAny: type-name mismatch",
324            });
325        }
326        T::decode(&self.payload)
327    }
328}
329
330impl zerodds_cdr::CdrEncode for DdsAny {
331    fn encode(
332        &self,
333        w: &mut zerodds_cdr::BufferWriter,
334    ) -> core::result::Result<(), zerodds_cdr::EncodeError> {
335        // Type-name als CDR-String + payload-bytes mit u32-length-prefix.
336        w.write_string(&self.type_name)?;
337        let payload_len = u32::try_from(self.payload.len()).map_err(|_| {
338            zerodds_cdr::EncodeError::ValueOutOfRange {
339                message: "DdsAny: payload > u32::MAX",
340            }
341        })?;
342        w.write_u32(payload_len)?;
343        w.write_bytes(&self.payload)?;
344        Ok(())
345    }
346}
347
348impl zerodds_cdr::CdrDecode for DdsAny {
349    fn decode(
350        r: &mut zerodds_cdr::BufferReader<'_>,
351    ) -> core::result::Result<Self, zerodds_cdr::DecodeError> {
352        let type_name = r.read_string()?;
353        let payload_len = r.read_u32()? as usize;
354        let payload = r.read_bytes(payload_len)?.to_vec();
355        Ok(Self { type_name, payload })
356    }
357}
358
359// ---------------------------------------------------------------------
360// Built-in `DdsType` fuer &[u8]/Vec<u8>-Payloads
361//
362// Viele ROS-Use-Cases und Interop-Tests brauchen "roh durchreichen".
363// Ein `BytesPayload`-Newtype mit festem Type-Name erlaubt das.
364// ---------------------------------------------------------------------
365
366/// Ein opaker Raw-Byte-Payload mit konfigurierbarem Type-Name (per
367/// `impl` von `BytesPayload<T>` oder via newtype).
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct RawBytes {
370    /// Payload-Bytes (werden 1:1 auf die Wire gelegt, kein CDR-Framing).
371    pub data: Vec<u8>,
372}
373
374impl RawBytes {
375    /// Konstruktor.
376    #[must_use]
377    pub fn new(data: Vec<u8>) -> Self {
378        Self { data }
379    }
380}
381
382impl DdsType for RawBytes {
383    const TYPE_NAME: &'static str = "zerodds::RawBytes";
384
385    fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
386        out.extend_from_slice(&self.data);
387        Ok(())
388    }
389
390    fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
391        Ok(Self {
392            data: bytes.to_vec(),
393        })
394    }
395}
396
397#[cfg(test)]
398#[allow(clippy::expect_used, clippy::unwrap_used)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn raw_bytes_roundtrip() {
404        let orig = RawBytes::new(vec![1, 2, 3, 4, 5]);
405        let mut buf = Vec::new();
406        orig.encode(&mut buf).unwrap();
407        let back = RawBytes::decode(&buf).unwrap();
408        assert_eq!(back, orig);
409    }
410
411    #[test]
412    fn raw_bytes_type_name_is_namespaced() {
413        assert_eq!(RawBytes::TYPE_NAME, "zerodds::RawBytes");
414    }
415
416    // ---- .B: keyed types + KeyHash ----
417
418    /// Test-Fixture: keyed Topic mit @key u32 id (max 4 byte → zero-pad).
419    struct SmallKeyed {
420        id: u32,
421    }
422
423    impl DdsType for SmallKeyed {
424        const TYPE_NAME: &'static str = "test::SmallKeyed";
425        const HAS_KEY: bool = true;
426        const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(4);
427
428        fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
429            out.extend_from_slice(&self.id.to_le_bytes());
430            Ok(())
431        }
432        fn decode(bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
433            if bytes.len() < 4 {
434                return Err(DecodeError::Invalid {
435                    what: "truncated SmallKeyed",
436                });
437            }
438            let id = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
439            Ok(Self { id })
440        }
441        fn encode_key_holder_be(&self, holder: &mut PlainCdr2BeKeyHolder) {
442            holder.write_u32(self.id);
443        }
444    }
445
446    #[test]
447    fn small_keyed_produces_zero_padded_keyhash() {
448        let s = SmallKeyed { id: 0x1122_3344 };
449        let key = s.compute_key_hash().expect("keyed");
450        assert_eq!(&key[0..4], &[0x11, 0x22, 0x33, 0x44]);
451        assert_eq!(&key[4..16], &[0u8; 12]);
452    }
453
454    #[test]
455    fn non_keyed_returns_none_for_keyhash() {
456        let r = RawBytes::new(vec![1, 2, 3]);
457        assert_eq!(r.compute_key_hash(), None);
458    }
459
460    #[test]
461    fn keyed_two_instances_have_distinct_hashes() {
462        let a = SmallKeyed { id: 1 };
463        let b = SmallKeyed { id: 2 };
464        assert_ne!(a.compute_key_hash(), b.compute_key_hash());
465    }
466
467    /// Test-Fixture: keyed Topic mit unbounded @key string (MD5-Pfad).
468    struct LargeKeyed {
469        topic: alloc::string::String,
470    }
471
472    impl DdsType for LargeKeyed {
473        const TYPE_NAME: &'static str = "test::LargeKeyed";
474        const HAS_KEY: bool = true;
475        const KEY_HOLDER_MAX_SIZE: Option<usize> = None; // unbounded → MD5
476
477        fn encode(&self, out: &mut Vec<u8>) -> core::result::Result<(), EncodeError> {
478            out.extend_from_slice(self.topic.as_bytes());
479            Ok(())
480        }
481        fn decode(_bytes: &[u8]) -> core::result::Result<Self, DecodeError> {
482            Err(DecodeError::Invalid {
483                what: "test fixture",
484            })
485        }
486        fn encode_key_holder_be(&self, holder: &mut PlainCdr2BeKeyHolder) {
487            holder.write_string(&self.topic);
488        }
489    }
490
491    #[test]
492    fn large_keyed_produces_md5_hashed_keyhash() {
493        let s = LargeKeyed {
494            topic: alloc::string::String::from("hello"),
495        };
496        let key = s.compute_key_hash().expect("keyed");
497        // 16 byte deterministic hash, ungleich zero
498        assert_ne!(key, [0u8; 16]);
499        // Idempotent
500        let key2 = s.compute_key_hash().expect("keyed");
501        assert_eq!(key, key2);
502    }
503
504    #[test]
505    fn spec_aligned_aliases_match_implementation_names() {
506        // zerodds-xcdr2-rust §11 Errata.
507        assert_eq!(
508            <RawBytes as DdsType>::IS_KEYED,
509            <RawBytes as DdsType>::HAS_KEY
510        );
511        fn is_keyed<T: DdsType>() -> bool {
512            T::IS_KEYED
513        }
514        assert!(is_keyed::<SmallKeyed>());
515        let s = SmallKeyed { id: 0xABCD };
516        assert_eq!(s.key_hash(), s.compute_key_hash());
517    }
518
519    #[test]
520    fn extensibility_default_is_final() {
521        assert_eq!(<RawBytes as DdsType>::EXTENSIBILITY, Extensibility::Final);
522        // ExtensibilityKind alias is the same type.
523        let _: ExtensibilityKind = Extensibility::Mutable;
524    }
525
526    #[test]
527    fn encode_be_default_delegates_to_encode() {
528        let r = RawBytes::new(vec![1, 2, 3]);
529        let mut le = Vec::new();
530        let mut be = Vec::new();
531        r.encode(&mut le).unwrap();
532        r.encode_be(&mut be).unwrap();
533        assert_eq!(le, be);
534    }
535
536    #[test]
537    fn keyed_member_order_matters() {
538        // Hypothetisch: zwei Members mit verschiedener Reihenfolge wuerden
539        // unterschiedliche Hashes ergeben. Wir verifizieren das mit einem
540        // Mock-Type, der zwei Felder in Reverse-Order schreibt.
541        struct A {
542            x: u32,
543            y: u32,
544        }
545        impl DdsType for A {
546            const TYPE_NAME: &'static str = "test::A";
547            const HAS_KEY: bool = true;
548            const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(8);
549            fn encode(&self, _out: &mut Vec<u8>) -> Result<(), EncodeError> {
550                Ok(())
551            }
552            fn decode(_b: &[u8]) -> Result<Self, DecodeError> {
553                Err(DecodeError::Invalid { what: "stub" })
554            }
555            fn encode_key_holder_be(&self, holder: &mut PlainCdr2BeKeyHolder) {
556                holder.write_u32(self.x);
557                holder.write_u32(self.y);
558            }
559        }
560        struct B {
561            x: u32,
562            y: u32,
563        }
564        impl DdsType for B {
565            const TYPE_NAME: &'static str = "test::B";
566            const HAS_KEY: bool = true;
567            const KEY_HOLDER_MAX_SIZE: Option<usize> = Some(8);
568            fn encode(&self, _out: &mut Vec<u8>) -> Result<(), EncodeError> {
569                Ok(())
570            }
571            fn decode(_b: &[u8]) -> Result<Self, DecodeError> {
572                Err(DecodeError::Invalid { what: "stub" })
573            }
574            fn encode_key_holder_be(&self, holder: &mut PlainCdr2BeKeyHolder) {
575                holder.write_u32(self.y);
576                holder.write_u32(self.x);
577            }
578        }
579        let a = A { x: 1, y: 2 };
580        let b = B { x: 1, y: 2 };
581        assert_ne!(a.compute_key_hash(), b.compute_key_hash());
582    }
583}