pub enum LengthCodePolicy {
Compact,
ReuseNestedDelimiter,
ShortWhereItFits,
UniversalNextInt,
SizedOrNextInt,
}Expand description
Which length code a @mutable member’s EMHEADER gets.
XTypes 1.3 §7.4.3.4.2 gives eight codes and constrains almost nothing about which one a writer picks. LC0 to LC3 state a length of 1, 2, 4 or 8 in the header itself; LC4 states it in a following NEXTINT; LC5 to LC7 say the member’s own leading word is the NEXTINT, times one, four or eight. Several are legal for the same member and they are not the same bytes, so which one is a configuration point.
§The three implementations take three positions
Measured 2026-08-09 through wire-oracle/gate, cases
mut_lengthcodes and mut_lc_nested, XCDR2 little endian. The
EMHEADER each member of a @mutable struct gets:
| member | Cyclone | RTI 7.7 | Fast DDS |
|---|---|---|---|
string s = "abc" | 00000050 LC5 | LC5 | LC5 |
sequence<octet> of 3 | 01000050 LC5 | LC5 | LC5 |
sequence<short> of 2 | 02000040 LC4 | LC4 | 02000030 LC3 |
sequence<long> of 2 | 03000060 LC6 | LC6 | LC6 |
sequence<double> of 1 | 04000070 LC7 | LC7 | LC7 |
sequence<Cell> of 1 | 05000050 LC5 | 05000040 LC4 | LC5 |
Cell arr[2] | 06000050 LC5 | LC4 | LC5 |
long ap[2] | 07000040 LC4 | LC4 | 07000030 LC3 |
Cell nested | 08000040 LC4 | LC4 | 08000030 LC3 |
long tail | 09000020 LC2 | LC2 | LC2 |
ACell app (@appendable) | 00000040 LC4 | 00000050 LC5 | LC5 |
MCell mnest (@mutable) | 01000040 LC4 | LC5 | LC5 |
The sequence<Cell> and Cell arr[2] rows are the collection
delimiter — CollectionDelimiterPolicy — seen from the length-code
side: RTI writes no collection DHEADER, so there is no leading word
to reuse and LC4 is the only code left. What is left over after that
is two independent choices, and each implementation makes a different
pair of them:
- Does a nested aggregate’s own DHEADER count as a reusable
leading word? An
@appendableor@mutablemember begins with a DHEADER, which is exactly the byte length LC5 would state. RTI and Fast DDS reuse it. Cyclone does not, and says why indds_cdrstream.c:case DDS_OP_VAL_UNI: case DDS_OP_VAL_EXT: return LENGTH_CODE_NEXTINT; /* FIXME: may be optimized for specific cases, e.g. when EXT type is appendable */. - Does a member with no reusable word take a short code when its
serialized size happens to be 1, 2, 4 or 8? Fast DDS does.
Cyclone and RTI take a short code only where the member’s kind
fixes the width — a primitive, an enumeration, a bitmask — and
write LC4 for an eight-byte array or an eight-byte nested
structure alike. Cyclone’s
get_length_codeswitches on the op kind and never on a size.
The two nest: Cyclone makes neither choice, RTI the first, Fast DDS both. So one enum in that order rather than two booleans, which would have four combinations and three witnesses.
§The runtime half of Fast DDS’s rule
Fast DDS’s sequence<short> of two elements is eight bytes and takes
LC3. Three elements would be ten and take LC4. A layout plan cannot
make that choice — the count is in the value, not in the type — so
Self::ShortWhereItFits is answered in two places: the plan
answers for every member whose size the type fixes, and the writer
answers for the rest by patching the header once the value’s length
is known. Writer::finish_emheader does the patching, which is the
same thing it already does to the NEXTINT and is safe for the same
reason: under XCDR2 MAXALIGN is four, the value starts four-aligned
with the NEXTINT and four-aligned without it, so removing the
placeholder moves the value without changing a byte of it.
§Every code decodes
A reader that implements §7.4.3.4.2 handles all eight, because the
code is in the header. This is a question about which bytes go out,
not about which can be read — unlike CollectionDelimiterPolicy,
where nothing in the stream says whether the four bytes in front are
a delimiter or the first element.
Variants§
Compact
LC5, LC6 and LC7 where a string’s or a sequence’s own leading word can be reused, LC0 to LC3 where the member’s kind fixes its width, and LC4 everywhere else.
The default, and what Cyclone DDS writes. A nested aggregate’s DHEADER is not reused and an eight-byte array is not shortened, which is where Cyclone parts from the other two.
ReuseNestedDelimiter
Self::Compact, and a nested aggregate that begins with its
own DHEADER reuses it as the NEXTINT.
What RTI Connext 7.7 writes. Four bytes shorter than
Self::Compact for every @appendable or @mutable member of
a @mutable type.
ShortWhereItFits
Self::ReuseNestedDelimiter, and any member with no reusable
leading word takes LC0 to LC3 whenever its serialized size turns
out to be 1, 2, 4 or 8 bytes.
What Fast DDS writes. The reuse is tried first: Fast DDS frames
an eight-byte string as LC5 and not LC3, so the leading word
wins where there is one.
Half of this is a writer-time decision — see the type’s documentation.
UniversalNextInt
LC4 with an explicit NEXTINT throughout, whatever the member’s size or shape. Four bytes more per member than any of the three above, and the form the pre-rebuild ZeroDDS emitters produced.
No measured implementation writes it. Reach for it when a peer
expects that form — the OMG wire vector V-10 at e1f3af895 is
pinned in exactly that shape — or when reproducing a recorded
capture byte for byte.
SizedOrNextInt
LC0 to LC3 wherever the member’s serialized length is exactly 1, 2, 4 or 8 octets, LC4 everywhere else, and never a reused leading word.
What OpenDDS writes, and the fourth of the four positions: it
answers the second question of the pair above yes and the first
no, which is the combination none of the three others is.
Measured on struct-mutable-length-codes, XCDR2, against this
project’s Self::Compact:
member ours OpenDDS
string s = "abc" LC5 LC3
sequence<octet> of 3 LC5 LC4, NEXTINT 7
sequence<short> of 2 LC4, NEXTINT 8 LC3
sequence<long> of 2 LC6 LC4, NEXTINT 12
sequence<double> of 1 LC7 LC4, NEXTINT 12
sequence<Cell> of 1 LC5 LC4
Cell pair[2] LC5 LC4
long plain[2] LC4 LC3
Cell nested LC4 LC3Every code on both sides is legal for the member it is on — §7.4.3.4.2 permits several for the same member — so this is a reading of the clause and not a defect, and it is the one of the four that never spends a code on a word the member already carries.
Half of it is a writer-time decision, for the same reason
Self::ShortWhereItFits’s is: string s is eight octets
because the sample says "abc", and no plan knows that. The plan
answers where the type fixes the size — long plain[2] and
Cell nested — and Writer::finish_emheader patches the rest.
Implementations§
Source§impl LengthCodePolicy
impl LengthCodePolicy
Sourcepub const fn is_default(self) -> bool
pub const fn is_default(self) -> bool
Whether this is the default.
Sourcepub const fn reuses_a_leading_word(self) -> bool
pub const fn reuses_a_leading_word(self) -> bool
Whether a member’s own leading word — a string’s length, a sequence’s count, a nested aggregate’s DHEADER — is ever spent as the NEXTINT.
Three of the five do it in some form; Self::UniversalNextInt
and Self::SizedOrNextInt never do, which is what leaves them
with LC0 to LC4 only.
Sourcepub const fn reuses_nested_delimiter(self) -> bool
pub const fn reuses_nested_delimiter(self) -> bool
Whether a nested aggregate’s own DHEADER is reused as the NEXTINT, which is LC5 over the member rather than LC4 in front of it.
Sourcepub const fn short_code_where_it_fits(self) -> bool
pub const fn short_code_where_it_fits(self) -> bool
Whether a member with no reusable leading word takes LC0 to LC3 when its serialized size is 1, 2, 4 or 8.
The plan applies it where the type fixes the size; the writer applies it to the rest.
Sourcepub fn parse(value: &str) -> Option<Self>
pub fn parse(value: &str) -> Option<Self>
Parse a configuration value.
Accepts the names LengthCodePolicy::as_str produces. An
unknown value returns None rather than falling back to the
default: a typo in a wire-affecting setting must be loud.