pub enum Dialect {
Cyclone,
Rti,
FastDds,
Specification,
OmniOrb,
Tao,
JacOrb,
OpenDds,
RosFastRtps,
RosCyclone,
OpenSplice,
Hdds,
}Expand description
Which implementation’s wire behaviour a plan reproduces.
Every setting below is separately selectable, and a dialect is a name for one whole set of them. It exists because a peer is not a collection of independent quirks: talking to RTI Connext means taking RTI’s answer to every question at once, and assembling that from five flags is five chances to get one wrong.
§A dialect name is a promise
--dialect opendds, on its own and with no second flag,
reproduces every OpenDDS byte string this project’s record holds.
The same for the other eight measured systems.
tools/interop-results/tests/dialect_completeness.rs is that claim
as a gate, row by row, and it is the reason several of the settings
below moved into a dialect: a name that reaches a product only if
the caller also knows which three further switches to set is a name
that has moved the question rather than answered it.
The consequence, stated once and applying throughout. Where a
product’s behaviour departs from the clause, the dialect named for
that product takes the product’s behaviour, including where that
behaviour is a defect. Fast DDS’s front end drops @bit_bound and
writes a four-octet enumeration holder; Cyclone matches no negative
case label on a discriminator narrower than 32 bits and writes no
branch at all; OpenDDS numbers a @mutable union’s branches from 0
and terminates nothing. Each of those is what the peer puts on the
wire, and a preset that named the peer and wrote something else
could not talk to it. Interoperability decides here;
Dialect::Specification is where the clause is, and it takes no
vendor’s departure.
Each such setting names, at its own method below, the measurement it rests on and the condition under which it leaves the dialect again — which is always the same condition: the product stops doing it, and then the entry goes with a version saying from when.
§The default is the specification, not a product
Where the implementations disagree, this project writes what the
clause says unless told otherwise. Self::Specification is
Dialect::default and Self::is_default answers for it.
docs/specs/zerodds-interop-suite-1.0.md §7 states it and §6 lists
it among what is settled.
This reverses an earlier decision, and the reversal is the point.
The default was Self::Cyclone, on the argument that a default has
to be some implementation’s behaviour to interoperate out of the
box. What that argument left out is which behaviour it hands to a
caller who names nothing: once the products’ defects moved into the
dialects that name those products — the pass
docs/wire-axes-followup.md records — the default started writing
Cyclone’s defects. A wstring’s code units in the host’s order
rather than the stream’s, and no branch at all for a negative case
label on a narrow signed discriminator.
Self::wstring_unit_order and Self::union_label carry the
runs. A caller who named no dialect got those two, and had nothing
in hand saying so.
A dialect is a deliberate choice to talk to one peer, and it may
carry that peer’s defects — that is what it is for. It must not be
what somebody gets who chose nothing. --dialect cyclone is a
dialect like the other eight now, complete in what it reproduces and
preferred in nothing.
Self::Specification is not what any measured implementation
produces in full, and that is the cost of the choice rather than an
argument against it. Held against each system’s own record it reaches
most of what each writes and departs where that system departs from
the text; the two result tables carry the per-system count and
tools/interop-results/tests/dialect_completeness.rs holds the claim
that each named dialect is complete for its own system.
Where it stands alone is measured and counted.
dialect_completeness.rs::the_default_writes_octets_no_measured_system_wrote
holds the default’s octets against every system’s in each of the 1332
recorded (case, version, order) slots: 84 of them have no witness
— no recorded system wrote what the default writes there.
no_single_axis_separates_the_default_from_every_measured_system says
the other half, and it is the one that matters for reading this
number: no axis is alone. Every value the default holds is held
by some product; what nobody was measured writing is the
combination. The largest family is map — Fast DDS is the only
system that produces a map’s octets and it orders the entries
ascending by key, so the clause’s declaration order has no witness in
46 slots. The next is an unannotated aggregate taken as
@appendable (RTI’s, Fast DDS’s and OpenDDS’s answer) beside a
holder in the declared @bit_bound (Cyclone’s and RTI’s) — each half
somebody’s, the pair nobody’s.
The same sweep over Self::Cyclone gives 147, and that
comparison is asserted rather than recalled: the default this one
replaced stood alone on more of the record, not less.
§What each dialect holds, and where the evidence is
Every cell is derived from the recorded octets rather than written
down: interop-results --dialects searches the axis product against
each system’s own rows and reports, per axis, the value that
reproduces them and whether any row pins it. A cell no row pins
is a question that system’s record does not ask, and it is named as
such at the method that answers it rather than left to look
measured.
The last column is the default. It is last because the table is read left to right against the products; nothing else follows from the order.
| setting | Cyclone | RTI 7.7 | Fast DDS | OpenDDS | Specification |
|---|---|---|---|---|---|
| collection delimiters | specification | never | enum-is-primitive | specification | specification |
a map’s delimiter | either half | either half | value only | either half | either half |
| PL_CDR1 headers | always-extended | extended-for-constructed-in-large-samples | short-true-length | short-true-length | specification |
| PL_CDR1 pid flags | sentinel-must-understand | sentinel-must-understand | bare | impl-extension-members | sentinel-must-understand |
| default extensibility | final | appendable | appendable | appendable | appendable |
wstring length, XCDR2 | octets/unterminated/2 | octets/unterminated/2 | characters/unterminated/2 | octets/unterminated/2 | octets/unterminated/2 |
wstring length, XCDR1 | octets/unterminated/2 | characters/terminated/4 | characters/unterminated/2 | octets/unterminated/2 | octets/unterminated/2 |
wstring unit order | host-unswapped | stream | stream | stream | stream |
scalar wchar width | 2 | 4 under XCDR1, 2 under XCDR2 | 2 | 2 | 2 |
| length codes | compact | reuse-nested-delimiter | short-where-it-fits | sized-or-nextint | compact |
| member must-understand | declared | declared | omitted | declared-and-key | declared-and-key |
| enumeration holder | declared | declared | bit-bound-dropped | bit-bound-dropped | declared |
| enumerator value | declared | declared | declared | ordinal | declared |
| union labels | negative-unmatched-below-32 | sign-extended | sign-extended | sign-extended | sign-extended |
@mutable union framing | specification | specification | specification | branches-from-zero | specification |
| short pid range | up-to-reserved | up-to-reserved | up-to-reserved | all-fourteen-bits | up-to-reserved |
map entry order | declaration | declaration | ascending-key | ascending-key | declaration |
@hashid counter | advances | skipped | advances | skipped | advances |
| key holder, XCDR2 | forced-final | forced-final | forced-final | forced-final | forced-final |
| key holder, XCDR1 | forced-final | as-declared | forced-final | forced-final | forced-final |
The three CORBA presets and the two ROS presets are in their own sections below, because most of this table is unreachable under them.
Fast DDS’s default extensibility is measured after all. It was
carried here as an unknown, on the ground that fastddsgen refuses
the common::Robot of GitHub issues #41 and #42. The interop
corpus answers it without that case and always has: every
-unannotated twin is a struct with no annotation, and Fast DDS
writes 30000000 in front of primitive-mixed-unannotated under
XCDR2 where Cyclone writes nothing. That is @appendable, from a
recorded row, and the footnote that called it a guess was reading
past its own corpus.
§Where a preset does not reproduce its vendor
Every recorded row of every measured system is reproduced by that system’s own dialect, and the gate above is what says so. What is left in this section is the two places where that is true of the bytes and not of every value they can carry, plus the questions no dialect can settle.
§Closed, and what each one was
Kept as a list rather than deleted, because “a preset reproduces its vendor” is a claim that has to be re-checkable against the run that settled it.
| what the preset wrote | what the vendor writes | where |
|---|---|---|
Dialect::Rti, a two-byte wchar under XCDR1 | four bytes, 61000000 | wchar_member, WcharWidth |
Dialect::FastDds, M_FLAG set in the EMHEADER | never set, 01000020 | mut_mustunderstand, MustUnderstandPolicy |
Dialect::FastDds, LC4 for an eight-byte sequence<short> | LC3, 02000030 | mut_lengthcodes, LengthCodePolicy |
Dialect::Cyclone, LC3 for long ap[2] and for an eight-byte nested Cell | LC4, 07000040 and 08000040 | mut_lengthcodes |
Dialect::Rti, the same two | LC4, the same two | mut_lengthcodes |
Dialect::Rti, LC4 over an @appendable nested member | LC5 over its own DHEADER, 00000050 | mut_lc_nested |
Dialect::FastDds, a DHEADER on sequence<bitmask> | none, 01000030 | mut_lc_enum, CollectionDelimiterPolicy |
Dialect::Cyclone, a wstring’s units in the stream’s order | the host’s, 61006200 big endian | WstringUnitOrder, 12 rows |
Dialect::Cyclone, a negative narrow case label matching | no branch written | UnionLabelMatch, 24 rows |
Dialect::FastDds, an enumeration in its declared bound | four octets whatever the bound | EnumHolderWidth, 52 rows |
Dialect::FastDds, a map in declaration order | ascending by key | MapEntryOrder, 32 rows |
Dialect::FastDds, a DHEADER on map<string, long> | none | MapDelimiter, 12 rows |
Dialect::JacOrb, a fixed<P,S>’s declared digit count | the value’s precision | FixedDigitCount, 6 rows |
| no dialect at all for OpenDDS | six settings between them | Self::OpenDds, 936 rows |
| no dialect at all for either ROS type support | two settings and a coupling defect | Self::RosFastRtps, Self::RosCyclone, 369 rows |
The last three lines are this pass. The ones above them were closed earlier and are kept for the same reason.
§Dialect::FastDds does not reproduce Fast DDS’s lost characters
One layer under the framing, and it is a value question rather
than a byte one. Fast CDR counts characters and writes each
four-byte wchar_t as two bytes, so a code point outside the BMP
goes out as its low sixteen bits: U+1D11E becomes d11e, which is
U+D11E, a Hangul syllable and a perfectly well-formed code point.
The peer reads a different character and has nothing to object to.
WstringLengthPolicy::CHARACTER_COUNT reproduces the length
convention and refuses the value, rather than truncating it.
Reproducing a vendor’s framing is what a dialect is for; reproducing
a data loss that leaves no trace in the bytes is this project putting
a different character on the wire and calling it interoperability.
So --dialect fastdds writes Fast DDS’s bytes for every wstring
Fast DDS does not corrupt, and errors on the ones it does. No
recorded row carries such a value, so this costs no row.
§Dialect::OpenDds takes the declared fixed<P,S> count, as OpenDDS writes it
OpenDDS 3.34’s serializer writes a fixed as
OpenDDS::FaceTypes::Fixed_T<P,S>, (P + 2) / 2 octets: the
declared count, measured 2026-10-07 through the FACE mapping
(0.00 under fixed<5,2> is 00000c, 7 is 00700c). Its
write_fixed(to_ace_fixed()) is the ValueWriter path, and
ACE_CDR::Fixed’s own CDR operator — the string’s digits,
FixedDigitCount::ValueStringDigits, unspecified_constructs.md
§3 — is not on its wire. No recorded row of the OpenDDS column has a
fixed: the probe builds the classic mapping, where tao_idl
refuses the type and the struct comes out without the member.
§No preset reproduces any vendor’s @hashid member ids
The one place where naming a dialect cannot help, because the three
implementations disagree with each other as well as with this
project. XTypes 1.3 §7.3.1.2.1.1 defines the hash; which name goes
into it is where they part. vendor_roundtrip.md, “@hashid member
ids are not agreed by anyone”, and the inventory’s I22. A preset that
picked one would be reproducing one vendor and diverging from the
clause and from the other two, with nothing to say which.
§Two extrapolations inside Dialect::Rti and Dialect::FastDds
Both in LengthCodePolicy, both from the same measurement, both
written down here rather than left in a code comment:
- a union inside a
@mutabletype. Cyclone’s source puts a union and a nested aggregate in one arm and returns LC4 for both. RTI and Fast DDS were measured reusing a nested aggregate’s DHEADER, and neither was measured on a union — no vendor row has one inside a@mutabletype. The two are treated alike, on the reading that one clause and one leading word cannot have two answers. - a delimited nested aggregate of exactly one, two, four or eight
bytes.
LengthCodePolicy::ShortWhereItFitstries the reuse first, because Fast DDS frames an eight-bytestringas LC5 and not LC3. Whether it would do the same for an eight-byte delimited aggregate is untested; the shape does not arise in the measured cases, where every delimited member is twelve bytes or more.
§Dialect::Rti reproduces RTI, including where RTI departs
Not a gap, and here because it reads like one. RTI keeps a @mutable
type’s parameter list in the key holder under XCDR1, so its KeyHash
is the MD5 of the framing and not the specification’s key
zero-padded. KeyHolderExtensibility carries the run and
Dialect::Rti selects it, under XCDR1 only. Two peers that disagree
here compute different handles for one instance, which is not a
formatting difference — so unlike the byte order above, the departure
is modelled rather than recorded.
§What a dialect deliberately does not set
A map’s entry count, which is MapLengthPolicy and is not a
Dialect method at all. XTypes 1.3 §7.4.1.1.4 and rule (15) of
§7.4.3.5.3 contradict each other; Fast DDS is the one implementation
in the corpus that produces a map’s octets, and its rows pin the
equivalence’s reading — the count is there under a delimiter as well
as without one. OpenDDS’s generated operator<< reads rule (15) the
other way, calling write_delimiter under version 2 and the
<< length in the else branch under version 1 — and since
2026-08-24 its own rows measure exactly that: map-key-enum and
map-value-struct under XCDR2 carry the DHEADER and no count,
map-key-long (primitive on both halves, undelimited) carries the
count. The opendds matrix configuration passes
--map-length rule-15-omits; the policy stays a probe flag rather
than a Dialect method because the two readings now have one
measured implementation each.
Which of a map’s two halves decides whether it is delimited is a
different question, is measured, and is MapDelimiter.
§The three ORB dialects
XCDR1 is CORBA CDR — CORBA 3.3 part 2 §9.3 — so an ORB is a
reference for the whole shared base and Self::OmniOrb,
Self::Tao and Self::JacOrb are presets like the other three.
They differ from them in reach rather than in kind: an ORB has no
encoding version 2, no @mutable, no DHEADER and no EMHEADER, so
most of the settings above are unreachable under them and take the
clause’s value. What they do reach is the wide-character family,
where CORBA and XTypes genuinely part.
| omniORB 4.3.3 | TAO 2.5.24 | JacORB 3.9 | |
|---|---|---|---|
wchar framing | giop-length-prefixed | giop-length-prefixed-member-only | giop-length-prefixed |
scalar wchar width | 2 | 4 | 2 |
wstring unit width | 2 | 4 | 2 |
wstring length | octets/unterminated | octets/unterminated | octets/unterminated |
wstring unit order | byte-order-mark | stream | big-endian-always |
fixed<P,S> digits | declared | declared, and unmeasured | value-precision |
| default extensibility | final | final | final |
JacORB’s digit count is the one CORBA-only setting a dialect took in
this pass: FixedDigitCount carries the six rows.
Self::Tao’s cell is not a measurement — tao_idl refuses
fixed outright, so TAO has no recorded row — and the section above
says what ACE’s runtime does instead.
§The two ROS 2 dialects
Neither is its vendor. rosidl_typesupport_fastrtps_c generates its
own serialisation rather than calling fastddsgen’s output, and
rmw_cyclonedds_cpp walks the introspection type support rather
than using Cyclone’s idlc; both parse @final, @key,
@optional and @id and drop them, so every ROS message is a
@final struct in encoding version 1 whatever its .idl says.
Almost every setting above is therefore unreachable under them and
takes the clause’s value.
What they do reach is the wide-character family, and there they part:
ros-fastrtps 2.2.2 | ros-cyclone 1.3.4 | |
|---|---|---|
wstring length | characters/unterminated/4 | characters/unterminated/4 |
wstring unit order | host-unswapped | stream |
scalar wchar width | 4 | 2 |
The width row is the one that made WcharWidth an axis: it was
read off WstringLengthPolicy until rmw_cyclonedds_cpp was
fitted, and one setting cannot answer e4003412 for a member and
four-octet units inside a wstring at once. The unit-order row is
measured for ros-fastrtps — it writes both byte orders and its
big-endian rows keep the units little-endian — and is not
separable for ros-cyclone, which writes the host’s order and takes
no argument for it, so its record has one order and the two answers
coincide in it. Self::wstring_unit_order says which of the two
that is and why.
The extensibility row is not a measurement and does not need to be:
CORBA IDL has no extensibility annotation, an ORB writes the members
in declaration order with no framing, and under XCDR1 — the only
encoding an ORB has — @appendable is byte-for-byte @final
(rule 29, the inventory’s H2). @final is what an ORB’s bytes are;
naming it Appendable would be true of the bytes and false of the
type.
Two cells are extrapolations and both are named where a reader will
look: JacORB’s unit order comes from its read direction, because
org.jacorb.orb.CDROutputStream has no setLittleEndian and every
JacORB byte string is big-endian, where the two candidate orders
coincide; and TAO’s unit order is unmeasured for the same reason in
reverse, because libACE here was built without
ACE_ENABLE_SWAP_ON_WRITE and TAO writes host order only.
WstringUnitOrder::BigEndianAlways carries the first and
Self::Tao the second.
§Why the wide-character policies are not named for their GIOP versions
The measurement makes the case for it and then takes it back. CORBA
§9.3.1.6 has exactly two wide-character encodings, one per GIOP
version, and the DDS implementations split along that line:
WstringLengthPolicy::SPECIFICATION is GIOP 1.2’s wstring and
WstringLengthPolicy::CHARACTER_COUNT_TERMINATED is GIOP 1.1’s, and
TAO reproduces each by changing only the GIOP minor version. Naming
them Giop11 and Giop12 would put the citation in the name.
Four things say not to.
- One of the forms belongs to no edition.
WstringLengthPolicy::CHARACTER_COUNT— Fast DDS’s — is a character count with no terminator, which is neither §9.3.1.6 edition. A naming scheme that covers three of five variants and leaves the other two mechanism-named is worse than five mechanism names, because it reads as if the two were unclassified rather than as if they were unclassifiable. - The GIOP version does not fix the whole form. It fixes the
length convention; the code-unit width is the transmission code
set, which it does not fix. omniORB and TAO are both GIOP 1.2 and
write two octets and four. That is why
WstringLengthPolicy::SPECIFICATION_WIDE_UNITSexists at all, and a variant calledGiop12sitting beside another calledGiop12is not a naming scheme. - The default would be named for the wrong specification. This
project implements XTypes; §7.4.1.1.2 is what
WstringLengthPolicy::SPECIFICATIONcites. That the clause coincides with GIOP 1.2’swstringis a derivation worth recording, not an identity — and it is not an identity across the pair, because XTypes’Char16is fixed-width, which is GIOP 1.1‘swchar. XTypes’ own combination is GIOP 1.2’swstringwith GIOP 1.1’swchar, a pairing no edition produces. A variant namedGiop12that selected it would be wrong in its own name. - A name should say what the bytes are. Somebody holding a length field of 3 where 4 was expected needs “character count, terminated”; “GIOP 1.1” sends them to a document. The citation belongs in the doc comment and in the table above, where it now is, and the equivalence is stated in both directions so it can be followed either way.
What the GIOP reading did change is the shape of the model rather
than the names in it: the length convention and the unit width are
two axes because §9.3.1.6 makes them two, and the byte order is a
third because the code-set rule makes it one.
WcharFraming and WstringUnitOrder are that reading, and
without it the three ORB presets would each have been a fourth
special case in one overloaded enum.
Variants§
Cyclone
Eclipse Cyclone DDS.
Rti
RTI Connext DDS 7.7.
FastDds
eProsima Fast DDS with Fast CDR 2.3.5.
Specification
The clause-by-clause reading of XTypes 1.3, and the default.
No measured implementation produces it in full, which is what
makes it the default rather than what argues against it: the
default is what the specifications say, and a dialect is a
deliberate choice to talk to one peer and to carry that peer’s
defects while doing it. Self::is_default and the
“Why the specification is the default” section of this type’s
documentation carry the reasoning.
OmniOrb
omniORB 4.3.3, over CORBA CDR — which is XCDR1.
A two-octet transmission code set, GIOP 1.2’s length-prefixed
wchar, and a byte-order mark inside every wstring.
Tao
TAO 2.5.24, over CORBA CDR.
A four-octet transmission code set and GIOP 1.2’s
length-prefixed wchar, which makes its wstring the one form
no DDS implementation writes: an octet count over wide units.
JacOrb
JacORB 3.9, over CORBA CDR.
A two-octet code set like omniORB’s and GIOP 1.2’s wchar, with
no byte-order mark: it reads the units big-endian whatever the
stream says.
OpenDds
OpenDDS 3.34.0 over OpenDDS::DCPS::Serializer.
Fast DDS’s XCDR1 @mutable framing with a parameter id no other
implementation writes — PID_IMPL_EXTENSION and
PID_MUST_UNDERSTAND set on every member — an XCDR2 length code
that is short where the serialised length fits and never reuses
a nested delimiter, @key implying must-understand as
§7.2.2.4.4.4.8 requires, and a front end that carries neither
@value nor @bit_bound as far as a serialiser.
RosFastRtps
rosidl_typesupport_fastrtps_c 2.2.2, ROS 2 Humble.
Not Fast DDS: it generates its own serialisation. Encoding
version 1 only, every message @final, a wstring counted in
characters over four-octet units whose octets stay in the host’s
order while the length swaps, and a four-octet scalar wchar
that does swap.
RosCyclone
rmw_cyclonedds_cpp 1.3.4, ROS 2 Humble.
Not Cyclone: it walks the introspection type support. The same
wstring cell as Self::RosFastRtps over units in the
stream’s order, and a two-octet scalar wchar beside those
four-octet units — the one implementation of the nine where the
two widths part, and the reason WcharWidth is an axis.
OpenSplice
OpenSplice Community Edition 6.9, over
DDS::OpenSplice::CdrTypeSupport.
idlpp -S -l cpp generates the standalone C++ mapping and
CdrTypeSupport::serialize writes it: XCDR1 in the host’s byte
order, and nothing else — no XTypes, so no encoding version 2
and no @mutable. tools/wireprobe/opensplice-idl.py rewrites a
corpus type into the IDL 3.5 idlpp reads only where every
construct means the same XCDR1 bytes there, and refuses the rest
by name: @mutable, @optional, @external,
@must_understand, an enumerator’s @value, an enum’s
@bit_bound below 32, bitmask, bitset, map, a keyed union
discriminator and a union as the topic type, and idlpp itself
refuses wchar and wstring outright.
One axis is measured: OpenSplice’s 2 rows of
union-discriminator-short and its unannotated twin — the one
byte order and encoding version it writes — put it beside RTI,
Fast DDS and OpenDDS on UnionLabelMatch::SignExtended — the
clause’s answer, and Self::Specification’s. Every other axis is
unpinned for this dialect: wchar/wstring, @mutable framing,
map, @bit_bound below 32, @value and fixed are exactly
the constructs the front end above cannot express, so no recorded
row asks any of them a question, and this preset takes the
clause’s answer on every one rather than guess at a form nobody
measured.
Hdds
HDDS, over the serialiser hddsgen gen rust writes into
encode_xcdr1_le / encode_xcdr2_le.
Opt-in, and the column measures its generated code and not its
runtime — see docs/adding-a-column.md step 3 and
tools/wireprobe/hdds-shim. hddsgen emits no big-endian
encoder at all, so half of every cell does not exist; this preset
carries no endianness of its own and the gate that sweeps it
reaches only the little-endian rows.
Four departures carry the column, and each has its own axis:
| departure | axis | rows |
|---|---|---|
| a collection’s elements follow one another with no padding between them, and align from their own first octet | ElementPadding::Packed | array-1d-struct-final, sequence-struct-final |
every aggregate is written as @final: no DHEADER, no EMHEADER, no parameter header | AggregateDelimiter::Never | struct-appendable, annotation-must-understand-unannotated |
…except in front of a sequence element of a non-@final type, under both versions | SequenceElementDelimiter::BeforeEachNonFinalElement | sequence-struct-appendable, empty-struct-in-sequence |
a wstring is UTF-8 with a NUL the length counts | WstringCodeUnits::Utf8 | wstring-member, wstring-unbounded-utf8 |
Three more answers come off axes that already existed:
WcharWidth::Four (char-and-wchar), EnumHolderWidth::BitBoundDroppedByFrontEnd
(combined-keyed-topic) and CollectionDelimiterPolicy::Never
(sequence-struct-final). What is left is unpinned and takes the
clause’s answer, each arm of each method saying which.
Implementations§
Source§impl Dialect
impl Dialect
Sourcepub const CORBA: [Self; 3]
pub const CORBA: [Self; 3]
Every dialect that is a CORBA ORB rather than a DDS implementation.
Named because the difference is not cosmetic: an ORB has no
encoding version 2 and no @mutable, so a test that sweeps every
dialect through every encoding has to know which of the two lists
it is in. Self::is_corba is the same fact per value.
Sourcepub const fn is_corba(self) -> bool
pub const fn is_corba(self) -> bool
Whether this dialect is a CORBA ORB.
An ORB implements CORBA CDR, which is XCDR1 and nothing else: CORBA 3.3 part 2 §9.3 has no delimiter header, no EMHEADER and no encoding version field.
Asking one of these presets for an XCDR2 plan is not refused,
and the reason is worth stating because a refusal was the first
instinct. Every setting the preset carries has an answer under
both encoding versions — the wide-character three because a
wchar and a wstring are the same under either, and the rest
because they take the clause’s value — so the plan that comes out
is well defined and the three columns are held to it by the
three-column gate, which runs these presets over every encoding.
What such a plan is not is a reproduction of any ORB: there is
no XCDR2 byte string from omniORB, TAO or JacORB to be right or
wrong about. Nothing is dropped and nothing is misplaced; the
combination simply has no foreign side, which is what this method
is for saying.
Sourcepub const fn is_default(self) -> bool
pub const fn is_default(self) -> bool
Whether this is the default.
Self::Specification, and the one place the answer is written
as a predicate rather than read off #[default]. A caller that
wants to know whether a plan is the shipped one asks here; a
caller that wants the plan itself takes Dialect::default.
Sourcepub fn parse(value: &str) -> Option<Self>
pub fn parse(value: &str) -> Option<Self>
Parse a configuration value.
An unknown value returns None rather than falling back to the
default: a typo in a wire-affecting setting must be loud.
Sourcepub const fn collection_delimiters(self) -> CollectionDelimiterPolicy
pub const fn collection_delimiters(self) -> CollectionDelimiterPolicy
Which collections carry a delimiter under XCDR2.
Sourcepub const fn length_codes(self) -> LengthCodePolicy
pub const fn length_codes(self) -> LengthCodePolicy
Which length code a @mutable member’s EMHEADER gets.
The three take three positions and LengthCodePolicy carries
the run they were measured in. This used to answer Compact for
all four, which made Dialect::Rti four bytes long on every
delimited nested member and Dialect::FastDds four bytes long on
every eight-byte one.
Sourcepub const fn pl_cdr1_headers(self) -> PlCdr1HeaderPolicy
pub const fn pl_cdr1_headers(self) -> PlCdr1HeaderPolicy
How a @mutable member is framed under XCDR1.
Sourcepub const fn pid_flags(self) -> PidFlagPolicy
pub const fn pid_flags(self) -> PidFlagPolicy
What flags a PL_CDR1 parameter id carries, at a member and at the sentinel.
PidFlagPolicy::ImplExtensionOnMembers is Dialect::OpenDds’s,
and it is in that dialect because it is what OpenDDS puts on the
wire. Measured on struct-mutable and
evolution-mutable-mustunderstand under XCDR1, both byte
orders: c0010004 c0020004 c0030004, PID_IMPL_EXTENSION and
PID_MUST_UNDERSTAND set on the declared member and on the two
undeclared ones alike, and a bare 3f02 sentinel after them.
It is reached by --pid-flags impl-extension-members as well,
for a peer that wants the point without the rest of the preset.
It leaves this dialect the day OpenDDS stops setting the two bits unconditionally, with the version it stopped in.
Sourcepub const fn enum_holder(self) -> EnumHolderWidth
pub const fn enum_holder(self) -> EnumHolderWidth
How wide an enumeration’s holder is.
EnumHolderWidth::BitBoundDroppedByFrontEnd is Fast DDS’s
and OpenDDS’s, and it is in both of their dialects. It is a
front-end defect and not a reading of §7.4.3.5.3 — M1 of
docs/wire-axes-followup.md has fastddsgen 4.3.0 emitting
enum class Colour : int32_t and BitBound = 32 for an
@bit_bound(8) enumeration, and opendds_idl not knowing the
symbol at all — and that is exactly why a dialect has to carry
it: a peer that writes four octets writes them whatever the
reason, and a reader of one octet is three octets out of step
from there on. Naming the product is asking to talk to the
product.
Pinned by 76 recorded rows, both encoding versions and both byte
orders: enum-bitbound-{8,16}-{plain,value} and their
unannotated twins, array-3d-enum, combined-everything and
combined-keyed-topic. It leaves those two dialects the day
either front end carries the annotation as far as a serialiser,
with the version it started in.
--enum-holder bit-bound-dropped reaches it as a single point.
Written out for all ten rather than a catch-all arm, so that an eleventh dialect does not silently inherit an answer nobody measured for it.
Sourcepub const fn union_label(self) -> UnionLabelMatch
pub const fn union_label(self) -> UnionLabelMatch
Whether a case label of a narrow signed discriminator can be negative.
UnionLabelMatch::NegativeLabelUnmatchedBelow32 is
Self::Cyclone’s, and it is in that dialect because it is
what Cyclone puts on the wire. It drops the selected branch, so
a conforming reader of the frame takes the member after the
union for the branch — which is precisely what a peer talking to
Cyclone has to expect, and what this dialect is a name for.
Pinned by 24 recorded rows, both encoding versions and both byte
orders. union-discriminator-short, XCDR1: Cyclone writes
c7cf0000 ffffffff where RTI, Fast DDS and OpenDDS write
c7cf0000 7929edff ffffffff. f3 read as 243 is not -13 and
cfc7 read as 53191 is not -12345; a negative label on a
32-bit discriminator matches and an unsigned narrow one matches,
only the pair does not.
tests/interop-corpus/union-discriminator-short/README.md and
combined-everything/README.md carry the runs.
Self::Cyclone is not Dialect::default, so naming the
dialect is what it takes to get this. It was the default once,
and this setting is one of the two that made the reversal:
a caller who named nothing got a frame with no branch in it. The
default is Self::Specification, which is
UnionLabelMatch::SignExtended, the clause. This setting
leaves this dialect the day Cyclone matches the label, with the
version it started in.
--union-labels negative-unmatched-below-32 reaches it as a
single point.
Written out for all ten rather than a catch-all arm, so that an eleventh dialect does not silently inherit an answer nobody measured for it.
Sourcepub const fn map_count_omitted_when_delimited(self) -> bool
pub const fn map_count_omitted_when_delimited(self) -> bool
Whether a delimited map omits its entry count — rule (15) of
§7.4.3.5.3 as written, against §7.4.1.1.4’s sequence
equivalence.
Measured on 2026-08-24, one implementation on each side:
OpenDDS’s rows carry the DHEADER and no count (map-key-enum,
map-value-struct under XCDR2) and the count where the map is
undelimited (map-key-long, primitive on both halves); Fast
DDS’s rows carry both. A bool for the reason
hashed_id_advances_the_counter is one: MapLengthPolicy
lives with the codec, not here, and ProbeRequest::in_dialect
maps the fact onto it.
Sourcepub const fn array_element_flattening(self) -> ArrayElementFlattening
pub const fn array_element_flattening(self) -> ArrayElementFlattening
Whether an array’s element is flattened before the delimiter
question. See ArrayElementFlattening; only OpenDDS stops at
the alias, measured on resolve-array-of-typedef-array.
Sourcepub const fn map_entries(self) -> MapEntryOrder
pub const fn map_entries(self) -> MapEntryOrder
In what order a map’s entries go on the wire.
MapEntryOrder::AscendingKey is Self::FastDds’s, and
it is in that dialect because it is what Fast DDS puts on the
wire. Judgement 3’s threshold — two independent implementations
make a position on an axis — is about whether the order is a
reading of the clause, and it is not: §7.4.3.5.3 rules (14) to
(16) give a map as a length and a run of pairs and say nothing
about their order, so this is the property of the std::map
fastddsgen chose. That makes it a compatibility setting, which
is exactly what belongs in the dialect of the product that
writes it.
Pinned by 32 recorded rows: map-key-long, map-key-char,
map-key-ulonglong, map-key-boolean and map-key-string,
their unannotated twins, both byte orders, both versions.
Self::OpenDds joined on 2026-08-24: its column now
records map octets, and they are ascending by key under the
key type’s own value order — map-key-long puts −1234567 ahead
of 1 ahead of 2147483647 (signed), map-key-ulonglong puts 7
ahead of 16045690984833335998 (unsigned; a signed reading would
reverse them). The property of the ordered container both code
generators chose, two products independently — still a
compatibility setting and not a reading of the clause, which
says nothing about order. The remaining dialects take the
declaration order and nothing measured says otherwise.
--map-entries ascending-key reaches it as a single point.
Written out for all ten rather than a catch-all arm, so that an eleventh dialect does not silently inherit an answer nobody measured for it.
Sourcepub const fn enumerator_value(self) -> EnumeratorValue
pub const fn enumerator_value(self) -> EnumeratorValue
Which integer an enumerator serialises as.
EnumeratorValue::Ordinal is OpenDDS’s, and it is in
Self::OpenDds for the reason
EnumHolderWidth::BitBoundDroppedByFrontEnd is in it: a front
end that drops @value still puts the ordinal on the wire, and
naming the product is asking to talk to the product.
Pinned by 40 recorded rows. enum-bitbound-8-value under
OpenDDS, XCDR1 big endian, once the holder width is out of the
way: 00000002 00000001 ab where @value(200) and @value(9)
were declared — the ordinals 2 and 1.
tao_idl drops @value too and Self::Tao does not take
this, which is a difference between a measurement and a
reading: all three ORB columns record every @value case as
NOFORM — an enumerator with an explicit @value, where CORBA's is its ordinal position —
so no ORB row separates the two values. A dialect that took it
there would be reproducing a front end nobody measured through
this corpus.
--enumerator-value ordinal reaches it as a single point.
Written out for all ten rather than a catch-all arm, so that an eleventh dialect does not silently inherit an answer nobody measured for it.
Sourcepub const fn union_framing(self) -> MutableUnionFraming
pub const fn union_framing(self) -> MutableUnionFraming
How a @mutable union numbers its branches, and whether its
XCDR1 parameter list is terminated.
MutableUnionFraming::BranchesFromZero is OpenDDS’s, and
it is in Self::OpenDds although it reproduces a frame in
which two parameters share member id 0 and nothing terminates
the list — a frame no conforming reader can walk. That was the
argument for keeping it out of every dialect, and it is the
argument for putting it in this one: the frame is what an
OpenDDS peer sends, and a preset that named the product and
wrote something else would be a preset that cannot talk to it.
Pinned by 8 recorded rows, union-mutable and its unannotated
twin in both versions and both orders.
--union-framing branches-from-zero reaches it as a single
point.
Written out for all ten rather than a catch-all arm, so that an eleventh dialect does not silently inherit an answer nobody measured for it.
Sourcepub const fn short_pid_range(self) -> ShortPidRange
pub const fn short_pid_range(self) -> ShortPidRange
Which member ids fit a PL_CDR1 short parameter header.
ShortPidRange::AllFourteenBits is OpenDDS’s: it writes
an id in the range §7.4.1.2.2 reserves in the short header,
where Cyclone, RTI and Fast DDS were all measured writing the
extended one. Pinned by 2 recorded rows under XCDR1.
--short-pid-range all-fourteen-bits reaches it as a single
point.
Written out for all ten rather than a catch-all arm, so that an eleventh dialect does not silently inherit an answer nobody measured for it.
Sourcepub const fn fixed_digits(self) -> FixedDigitCount
pub const fn fixed_digits(self) -> FixedDigitCount
Whether a fixed<P,S>’s digit count comes from the declaration
or from the value.
FixedDigitCount::ValuePrecision is Self::JacOrb’s,
and it is in that dialect because it is what JacORB puts on the
wire: BigDecimal.precision() digits and never fewer than one.
Pinned by 6 recorded rows — fixed-basic, sequence-fixed,
array-1d-fixed and their twins, where JacORB writes
123456789c1dab against omniORB’s and this project’s
123456789c000000001dab — and they were the whole of JacORB’s
unmatched column.
FixedDigitCount::ValueStringDigits is no dialect’s. It is what
ACE/TAO 2.5.24’s ACE_CDR::Fixed writes through its own CDR
operator (the 7 rows of unspecified_constructs.md §3), and no
DDS product’s serializer goes through that operator: OpenDDS 3.34
writes OpenDDS::FaceTypes::Fixed_T<P,S>, (P + 2) / 2 octets,
the declared count — measured 2026-10-07 through the FACE mapping,
0.00 under fixed<5,2> as 00000c, 7 as 00700c, 0.12345
under fixed<6,5> as 0012345c. Its write_fixed(to_ace_fixed())
is the ValueWriter path (JSON and dynamic data), not the wire.
--fixed-digits value-precision reaches JacORB’s as a single
point; --fixed-digits value-string-digits reaches ACE’s.
Written out for all ten rather than a catch-all arm, so that an eleventh dialect does not silently inherit an answer nobody measured for it.
Sourcepub const fn must_understand(self) -> MustUnderstandPolicy
pub const fn must_understand(self) -> MustUnderstandPolicy
Whether a member’s must_understand reaches the wire, in either
encoding version.
Dialect::Specification takes
MustUnderstandPolicy::DeclaredAndKey and the three vendor
dialects do not, which is the one place in this table where the
specification’s answer and every measured vendor’s part company:
§7.2.2.4.4.4.8 says a @key member always has the attribute, and
Cyclone, RTI and Fast DDS were all measured writing
evolution-mutable-key’s two key members with M_FLAG clear.
OpenDDS writes them with it set.
Sourcepub const fn default_extensibility(self) -> Extensibility
pub const fn default_extensibility(self) -> Extensibility
What an aggregate whose source said nothing is taken to be.
Sourcepub const fn hashed_id_advances_the_counter(self) -> bool
pub const fn hashed_id_advances_the_counter(self) -> bool
Whether a @hashid member advances the sequential counter the
next unannotated member is numbered from.
DDS-XTypes 1.3 §7.3.1.2.1.1 says it twice and not the same way. One sentence numbers a member from “the most-recently specified ID (using the @id annotation …)”, which leaves a hashed id out of the count; the next numbers it from “the member ID of the previous member of the same Aggregated Type plus one”, which does not.
Measured 2026-08-09,
crates/ast-layout/tests/fixtures/vendor_bytes.md §7. For
@mutable struct Sample { @hashid long alpha; long b; } Cyclone
and Fast DDS give b the id 0343172D, the hash plus one; RTI
gives it 00000000. With an @id(10) before the hashed member
RTI gives 0000000B, so its counter is not merely reset — a
hashed id never advances it at all.
Three of the four take the second reading, which is why it is the default. It changes the member id on the wire, and a peer that numbers differently reads the wrong member or none, so RTI’s answer is a dialect and not a footnote.
§Why this one is a bool and not a policy enum
It is consumed by the lowering pass, in
zerodds_idl_lower::complete::member_ids, not by the planner:
member ids are fixed before a LayoutKey is ever built. A shared
policy type would have to live in a crate both zerodds-ast-layout
and zerodds-idl-lower can see, and neither depends on the other.
The fact still lives here, once, beside the other seven, and
zerodds_idl_lower::Options carries the boolean.
Sourcepub const fn admits_the_name_of_the_enclosing_scope(self) -> bool
pub const fn admits_the_name_of_the_enclosing_scope(self) -> bool
Whether a declaration may carry the name of the scope it is
written in — module common { struct common { … }; }.
IDL 4.2 §7.5.2 does not admit it: “The name of a module,
structure, union, map, interface, value type, event type,
exception or home may not be redefined within the immediate
scope of” that same construct. The products split on it, and the
split is measured by the corpus case
resolve-module-named-as-type in
tests/interop-results/ledger.tsv, each with its own words:
- Cyclone DDS —
Declaration 'common' collides with an earlier declaration of 'common' - OpenDDS —
Error - tao_idl: illegal redefinition "common" and "common", and TAO, omniORB and JacORB refuse it likewise - Fast DDS and RTI Connext take it and write octets
rosidltakes it too, under either middleware, because it runs its own front end rather than the vendor’s
A bool for the reason the two above are: the lowering pass
consumes it, before any LayoutKey exists.
zerodds_idl_lower::NameOfTheEnclosingScope carries the policy.
A reader that refuses the file can never reproduce the octets the
other four wrote — which is what
tools/interop-results/tests/dialect_completeness.rs measures,
and what this axis answers.
Sourcepub const fn hashid_reads_the_escaped_spelling(self) -> bool
pub const fn hashid_reads_the_escaped_spelling(self) -> bool
Whether a member-id hash reads an escaped name as written —
_long with its underscore — instead of the identifier the
escape spells.
IDL 4.2 §7.2.3.2 makes the underscore “a purely lexical
convention” that is not part of the name, so the specification
hashes long. RTI hashes the spelling, keyword or not: measured
with rtiddsgen 7.7.0 on @hashid long _long; and
@hashid long _foo;, whose TypeCode member ids are
79659193 = hash(“_long”) and 230782087 = hash(“_foo”) where the
clause gives 56906255 and 186170796 — rtiddsgen’s generated C
even names the fields _long and _foo. It changes the member
id on the wire, so it is a dialect and not a footnote.
A bool for the reason hashed_id_advances_the_counter is one:
consumed by the lowering pass, before any LayoutKey exists.
zerodds_idl_lower::HashedNameSpelling carries the policy.
Sourcepub const fn key_holder_extensibility(
self,
encoding: Encoding,
) -> KeyHolderExtensibility
pub const fn key_holder_extensibility( self, encoding: Encoding, ) -> KeyHolderExtensibility
Whether a key holder is forced to @final, which for RTI
depends on the encoding version.
Sourcepub const fn key_holder_nesting(self, encoding: Encoding) -> KeyHolderNesting
pub const fn key_holder_nesting(self, encoding: Encoding) -> KeyHolderNesting
How deep Step 1’s member removal reaches, which for RTI depends on the encoding version.
Sourcepub const fn key_holder_order(self, encoding: Encoding) -> KeyHolderOrder
pub const fn key_holder_order(self, encoding: Encoding) -> KeyHolderOrder
In what order a key holder’s members are serialized, which for RTI depends on the encoding version.
Sourcepub const fn key_holder_alignment(
self,
encoding: Encoding,
) -> KeyHolderAlignment
pub const fn key_holder_alignment( self, encoding: Encoding, ) -> KeyHolderAlignment
How a key holder’s members are aligned, which for Cyclone and RTI
depends on the encoding version. See KeyHolderAlignment for the
measurement.
Sourcepub const fn defaulted_discriminator_member(
self,
) -> DefaultedDiscriminatorMember
pub const fn defaulted_discriminator_member( self, ) -> DefaultedDiscriminatorMember
What becomes of a union’s member when @try_construct(use_default)
defaulted its discriminator.
Every dialect answers DefaultedDiscriminatorMember::TypeDefault,
and the twelve arms are written out rather than folded together
so that a thirteenth does not inherit an answer nobody looked
for.
RtiandOpenDdsare measurements: each hands back the member’s type default, recorded inDefaultedDiscriminatorMember’s table.FastDdscannot reach the question — it refuses an annotation on a union discriminator — so it takes the clause’s value and claims no measurement.Cycloneand the two ROS type supports have no recorded run of this shape either; the clause’s value, and nothing measured.- The three ORBs have no
@try_constructat all: CORBA CDR knows no such annotation, and a union whose discriminator value has no case is anINV_OBJREFthere, not an evolution. OpenSplicehas neither:opensplice-idl.pyrefuses@try_constructby name. The clause’s value.Hddshas not been asked:annotation-try-constructis one of the cases whose generated code does not compile. The clause’s value.
Sourcepub const fn minimal_member_trailing_octet(self) -> bool
pub const fn minimal_member_trailing_octet(self) -> bool
Whether this dialect writes the RTI Connext/CoreDX minimal
TypeObject camp — one trailing octet inside each aggregated
member’s DHEADER, counted into the byte stream the minimal
EquivalenceHash of XTypes 1.3 §7.3.4.5 is taken over.
docs/omg-xtypes-issues.md §C2 measures the split. Two camps
ship and neither stream is malformed; the clause does not decide
which one the hash covers, so this is a dialect choice and not a
defect in either sender.
RtiandCoreDx(CoreDX rides theRtiarm; the interop matrix records the two computing the same minimal hash) write the octet — measuredCyclone\u{2194}RTIandZeroDDS\u{2194}RTIonstruct_key_enum_1/struct_key_struct_1.Cyclone,FastDdsandSpecificationdo not: Cyclone and Fast DDS agree with each other and are the default here, andcrates/types/tests/typeobject_vendor_dheader.rsrecords their octets.- The ORBs and ROS type supports have no minimal
TypeObject(CORBA CDR and the ROS introspection paths carry none), so they take the defaultfalseand claim no measurement.
Sourcepub const fn wstring_length(self, encoding: Encoding) -> WstringLengthPolicy
pub const fn wstring_length(self, encoding: Encoding) -> WstringLengthPolicy
How a wstring’s length is counted, which for RTI depends on
the encoding version.
Sourcepub const fn wchar_framing(self) -> WcharFraming
pub const fn wchar_framing(self) -> WcharFraming
How a wchar is framed.
The setting that splits the five DDS implementations from the
three ORBs: every DDS implementation writes XTypes Table 31’s
fixed-width code unit and every ORB writes CORBA §9.3.1.6’s
GIOP 1.2 length-prefixed one for a member. The three ORBs then
part on the collection element — omniORB and JacORB keep the
count octet, TAO drops it — which is why TAO’s answer is
WcharFraming::GiopLengthPrefixedMemberOnly and not the same
value as the other two. WcharFraming has the runs.
Sourcepub const fn wchar_width(self, encoding: Encoding) -> WcharWidth
pub const fn wchar_width(self, encoding: Encoding) -> WcharWidth
How wide a scalar wchar is.
Eight of the nine give the same answer here as they give for a
wstring’s code units, which is why this was one setting until
the ninth was fitted: rmw_cyclonedds_cpp writes e400 3412
for primitive-wchar and four-octet units inside a wstring.
WcharWidth carries the octets.
RTI is the reason the encoding is an argument: 61000000 under
XCDR1 and 6100 under XCDR2, the same split its wstring takes.
Sourcepub const fn map_delimiter(self) -> MapDelimiter
pub const fn map_delimiter(self) -> MapDelimiter
Which of a map’s halves decides whether the map is delimited.
MapDelimiter carries the twelve Fast DDS rows that separate
the two readings. Every other measured implementation refuses a
map before it reaches a wire, so the clause’s reading is what
they take.
Sourcepub const fn wstring_unit_order(self) -> WstringUnitOrder
pub const fn wstring_unit_order(self) -> WstringUnitOrder
In what byte order a wstring’s code units go out.
WstringUnitOrder has the runs, including which of the three
ORB answers is measured in the write direction and which is
carried over from the read direction.
Sourcepub const fn element_padding(self) -> ElementPadding
pub const fn element_padding(self) -> ElementPadding
Whether the elements of a collection carry alignment padding between them.
ElementPadding::Packed is Self::Hdds’s and is
measured on array-1d-struct-final and sequence-struct-final,
which name the departure in their own octets. Every other dialect
answers the clause, and nothing else in this table has been
measured to differ.
Sourcepub const fn aggregate_delimiter(self, encoding: Encoding) -> AggregateDelimiter
pub const fn aggregate_delimiter(self, encoding: Encoding) -> AggregateDelimiter
Whether an aggregate’s declared extensibility reaches the wire.
AggregateDelimiter::Never is Self::Hdds’s and is
measured at three of the four positions an aggregate can be
reached in — the sample itself, a struct member, and a @mutable
type’s member framing. The fourth, a sequence element, is
Self::sequence_element_delimiter’s and answers the opposite,
which is why it is a separate axis and not a third value here.
Under the plain and delimited encodings only. Every recorded
HDDS row is plain-cdr1 or plain-cdr2; no HDDS bytes exist
under PL_CDR1 or PL_CDR2, where the encapsulation identifier states
a parameter list and a reader takes the framing from it. There the
dialect writes and reads the specification’s framing: measured on
the Linux test host, Never under PL_CDR1 refused Cyclone’s @mutable stream
in dialect_roundtrip.rs and wrote a parameter list with no
parameter headers in silent_drop.rs.
Sourcepub const fn sequence_element_delimiter(self) -> SequenceElementDelimiter
pub const fn sequence_element_delimiter(self) -> SequenceElementDelimiter
Whether a sequence element of a non-@final type is preceded
by its own octet length.
SequenceElementDelimiter::BeforeEachNonFinalElement is
Self::Hdds’s, measured on sequence-struct-appendable,
empty-struct-in-sequence, resolve-forward-declared,
recursive-mutual and recursive-self-sequence.
Sourcepub const fn wstring_code_units(self) -> WstringCodeUnits
pub const fn wstring_code_units(self) -> WstringCodeUnits
Which octets stand for a wstring’s characters.
WstringCodeUnits::Utf8 is Self::Hdds’s and is
measured on wstring-member and wstring-unbounded-utf8. It is
not the same question as Self::wstring_length: that one says
what the length counts, this one says what the content is made
of, and the two part because a run of UTF-8 octets has no
fixed-width code unit for the first to count.