pub enum WstringLengthCounts {
Octets,
Characters,
}Expand description
How a wstring’s length is counted, and how wide one character is.
XTypes 1.3 is not ambiguous here. §7.4.1.1.2: “The serialized length
of an object of type String<Char16> shall be the number of bytes in
the CDR buffer taken by the String<Char16> characters. This is twice
the number of characters in the string”, and “The representation
shall also not include any terminating NUL character(s).”
§7.4.3.5.3 rule (4) writes O.ssize as a UInt32 and then the
Char16 units, annotated “No NUL”. The three implementations write
three different things anyway.
§The three implementations disagree
Measured on 2026-08-09 from @final struct WStr { wstring w; octet tail; }, little endian, the w member’s bytes only:
w = "ab", XCDR2 | w = "ab", XCDR1 | |
|---|---|---|
| Cyclone | 04000000 61006200 | 04000000 61006200 |
| RTI 7.7 | 04000000 61006200 | 03000000 61000000 62000000 00000000 |
| Fast DDS | 02000000 61006200 | 02000000 61006200 |
w = "𝄞" (U+1D11E), XCDR2 | XCDR1 | |
|---|---|---|
| Cyclone | 04000000 34d81edd | 04000000 34d81edd |
| RTI 7.7 | 04000000 34d81edd | 03000000 34d80000 1edd0000 00000000 |
| Fast DDS | 01000000 1ed1 | 01000000 1ed1 |
The runs are in tests/fixtures/vendor_bytes.md. Fast DDS’s shape
is in its own source rather than in the harness: Fast CDR 2.3.5
src/cpp/Cdr.cpp:884 computes wstrlen * 2 for the capacity check
and then serialises wstrlen, and writes each wchar_t as two
bytes. wchar_t is four bytes wide on Linux, so U+1D11E leaves as
its low sixteen bits, d11e. Past the BMP that stops being a length
convention and becomes a lost character.
U+D11E is a Hangul syllable and a perfectly well-formed code
point. Nothing downstream can see that it is the wrong one: the
truncation happened in the sender and left no trace in the bytes.
Only where the low sixteen bits land in D800..=DFFF does the
result fail to be UTF-16 and get refused, and that is luck rather
than a check.
§The reader takes the configured form and never infers one
The forms are not separable from the bytes. Fast DDS’s encoding of
@final struct { wstring w; long n; } with w = "ab", n = 7 is
02000000 61006200 07000000; the specification’s encoding of the
same type with w = "a", n = 7 is 02000000 6100, two bytes of
padding, 07000000. Twelve bytes each, differing only in the
content of padding that no clause fixes and that RTI is measured
leaving uninitialised. A reader choosing between those two samples
would not be resolving an ambiguity, it would be inventing an
answer, and it would be taking it from the length field — the field
a hostile peer controls, and the one a reader must never let decide
how much it reads. So this is a setting, not a detection.
What the reader owes in exchange is that every mismatch it can see is an error and not a shorter string:
- a length the configured form cannot have produced is refused, and
the error names the form the length does fit and the setting that
selects it. RTI’s XCDR1 count of 3 and Fast DDS’s odd character
counts are both odd byte counts under
WstringLengthPolicy::SPECIFICATION; - code units that are not well-formed UTF-16 are refused rather than replaced. Substituting U+FFFD would hand the caller a character the sender never had;
- whatever is left at the end of the frame has to be padding that alignment accounts for, so a short read that shifts every later member ends as an error rather than as a plausible sample.
Two cases survive all three. An even character count of BMP text from Fast DDS, where the next member’s alignment absorbs the bytes the short read left — the twelve bytes above are that case. And a non-BMP code point Fast DDS truncated into a valid BMP one, which arrives as a well-formed string of the right length holding the wrong character. Both are undecidable from the stream, and they are the reason this is a setting at all.
§The width reaches a scalar wchar as well
WstringLengthPolicy::character_width is not a wstring detail. A Char16 is
one type and each implementation writes it at one width wherever it
appears. Measured 2026-08-09, case wchar_member,
@final struct WCh { wchar c; octet tail; } with c = U+0061:
| XCDR1 LE | XCDR1 BE | XCDR2 LE | XCDR2 BE | |
|---|---|---|---|---|
| Cyclone | 6100 ab | 0061 ab | 6100 ab | 0061 ab |
| RTI 7.7 | 61000000 ab | 00000061 ab | 6100 ab | 0061 ab |
| Fast DDS | 6100 ab | 0061 ab | 6100 ab | 0061 ab |
RTI’s four-byte XCDR1 wchar is the same four-byte character its
XCDR1 wstring writes, and its two-byte XCDR2 wchar is the same
two-byte character its XCDR2 wstring writes. So the scalar takes
WstringLengthPolicy::character_width and there is no second setting: a policy
that said “four-byte wstring characters, two-byte wchar” would
name no implementation.
This is why LayoutKey::offset_key carries the policy. A
wstring plans as crate::Extent::Unbounded under every variant
and moves no offset, but a wchar is a sized primitive: at four
bytes it aligns to four and shifts every member after it. A cache
that served a two-byte plan to a four-byte request would put them
all in the wrong place.
A code point above the BMP has no wchar in any variant
(§7.2.2.2.1.2.5) — the four-byte form zero-extends a code unit,
it does not carry a code point.
C1 — what a wstring’s length prefix counts.
One of the three questions WstringLengthPolicy used to answer at
once. Separated because implementations answer them independently:
see that type’s measured table, where two implementations agree on
C1 and part on C3.
Variants§
Octets
The number of octets the code units occupy.
XTypes 1.3 §7.4.1.1.2, and CORBA 3.3 part 2 §9.3.1.6’s GIOP 1.2
wstring. Cyclone, OpenDDS, omniORB, TAO and JacORB.
Characters
The number of code units, whatever each one weighs.
CORBA §9.3.1.6’s GIOP 1.1 wstring, and Fast CDR’s wcslen.
RTI under XCDR1, Fast DDS, and both ROS type supports.