pub enum WstringUnitOrder {
Stream,
BigEndianAlways,
ByteOrderMark,
HostUnswapped,
}Expand description
In what byte order a wstring’s code units go out, and whether a
byte-order mark travels with them.
One axis and one decision, because the two halves are one: an implementation that writes a mark reads the units by it, and one that writes no mark has to have a rule for a stream that carries none.
§What was measured
The write direction, 2026-08-10 on the Linux test host,
@final struct Sample { wstring w; octet tail; } with w = "a𝄞b":
little-endian stream
zerodds 08000000 6100 34d8 1edd 6200 ab
omniorb 0a000000 fffe 6100 34d8 1edd 6200 ab
big-endian stream
zerodds 00000008 0061 d834 dd1e 0062 ab
omniorb 00000010 fffe e400 … ("äöü ሴ Z", the long case)omniORB swaps the length word into the stream’s order and leaves
the mark and the content alone, always little-endian on this x86-64
host. In a big-endian stream its mark therefore reads as U+FFFE,
the byte-swapped mark, which is precisely the signal that the units
that follow are opposite to the stream — so omniORB’s output is
self-describing where Cyclone’s identically-ordered output
(WstringLengthPolicy::UnswappedUnits) is not.
The read direction, from docs/corba-column.md’s array-1d-wstring
run over a little-endian stream carrying no mark:
zerodds {"items":["","äZ","ሴ"]}
omniorb {"items":["","娀","㐒"]}
jacorb {"items":["","娀","㐒"]}Both ORBs decode the units as UTF-16 big-endian whatever the
stream says. That is not a defect in either: XTypes §7.4.1.1.2 makes
the units the stream’s through ESWAP (row A10 of the inventory),
and the OMG code-set rule for a GIOP 1.2 wstring makes them
UTF-16’s own — big-endian in the absence of a mark. Each side is
self-consistent and they cannot talk, and the failure is the silent
kind: a wstring comes back well formed, of the right length, and
holding different characters.
§Why this is a second setting and not a fifth WstringLengthPolicy
Because it crosses the length convention rather than extending it.
omniORB’s mark rides on the octet count of
WstringLengthPolicy::SPECIFICATION; nothing stops a GIOP 1.1
count from carrying one too. Folding the order into the length enum
is what produced WstringLengthPolicy::UnswappedUnits, which is
this axis’s little-endian point wearing a length policy’s name, and
doing it again would give eight variants for two questions.
WstringLengthPolicy::UnswappedUnits therefore stays where it
shipped, and the pair (UnswappedUnits, anything but [Self::Stream]) is refused rather than resolved — see
WstringUnitOrder::conflicts_with_length, which is retired. Two settings that both
decide one byte order, with a precedence rule between them, is the
shape that produces a wrong character and no error.
Variants§
Stream
The stream’s own byte order, no mark.
XTypes 1.3 §7.4.1.1.2 through ESWAP, and what Cyclone, RTI and
Fast DDS write in little endian and RTI and Fast DDS write in
big. The default.
BigEndianAlways
UTF-16 big-endian whatever the stream says, no mark.
What omniORB and JacORB decode, and the CORBA rule for a
GIOP 1.2 wstring that carries no mark. Reachable as
--wstring-units big-endian-always and selected by
Dialect::JacOrb.
JacORB’s write direction cannot tell this apart from
Self::Stream: org.jacorb.orb.CDROutputStream has no
setLittleEndian, so every JacORB byte string measured here is
big-endian and the two forms coincide in it. What is measured is
JacORB’s reader, which takes the units big-endian out of a
little-endian stream — so this is the form a JacORB peer reads
correctly, which is the half that decides what a writer should
do. Recorded as an extrapolation from the read direction, in the
place a reader of Dialect::JacOrb will look for it.
ByteOrderMark
A byte-order mark, then the units, both little-endian.
What omniORB 4.3.3 writes. The mark is U+FEFF, its two octets
are counted in the length, and it is not a character of the
string.
On read the mark decides: FEFF in the stream’s order means
the units follow in the stream’s order, FFFE means they are
opposite, and no mark at all means big-endian — the CORBA rule,
and the one that makes this variant read a Self::BigEndianAlways
peer as well as an omniORB one.
On write the mark and the units are little-endian, because
that is what was measured, on x86-64, in both stream orders.
“Little-endian” and not “the host’s order” for the reason
WstringLengthPolicy::UnswappedUnits gives at more length: a
big-endian host running omniORB would presumably write big-endian
units and a plain FEFF, and that is not something a peer on a
little-endian host can produce or this project can measure.
XTypes §7.4.1.1.2 forbids the mark outright — “The representation shall also not include any … byte-order mark” — so no dialect of a DDS implementation selects this and the default never will.
HostUnswapped
The units always little-endian, whatever the stream says, and no mark.
This point reproduces a peer misreading its own output, and
it is on this axis because it is a byte order. It arrived as
WstringLengthPolicy::UnswappedUnits — a byte order wearing a
length policy’s name — and judgement 2 of
docs/wire-axes-followup.md moved it here, which also retired
the (UnswappedUnits, non-Stream) conflict rule: two settings
that both decide one byte order, with a precedence between them,
is the shape that produces a wrong character and no error. One
axis cannot conflict with itself.
Cyclone swaps the length word for a big-endian stream and does
not swap the content, so its big-endian "ab" leaves as
00000004 61006200 where RTI and Fast DDS write
00000004 00610062. Read as the big-endian stream the
encapsulation identifier says it is, Cyclone’s "ab" is U+6100
U+6200 — two different characters, well-formed UTF-16, the right
length, and nothing in the frame saying so. Cyclone reads its own
bytes back correctly because it unswaps them the same way; every
conforming peer gets the other string.
rosidl_typesupport_fastrtps does the same, measured
2026-08-12: wstring-unbounded-utf8 big endian is
00000007 e4000000 …, a big-endian length over little-endian
four-octet units. Two implementations, and the second one is
what makes this a point on the axis rather than one product’s
defect.
In little endian it is byte for byte Self::Stream, which is
why the divergence took a big-endian run to find.
“Little-endian” and not “the host’s order” because that is what
was measured: Cyclone memcpys its native UTF-16 buffer, and
every run is on x86-64. A big-endian host would presumably write
big-endian units and be byte-identical to Self::Stream there
— unmeasured, and not something a peer on a little-endian host
can produce.
No Dialect takes it as a default, including
Dialect::Cyclone: a default has to be the form a conforming
reader gets right. It exists because refusing to model it means
nobody can exchange a wstring with Cyclone over big endian at
all, and “we decline to talk to that peer” is not a wire question
the project gets to leave unanswered.
Implementations§
Source§impl WstringUnitOrder
impl WstringUnitOrder
Sourcepub const fn is_default(self) -> bool
pub const fn is_default(self) -> bool
Whether this is the default.
Sourcepub const fn writes_little_endian_units(self, _position: WirePosition) -> bool
pub const fn writes_little_endian_units(self, _position: WirePosition) -> bool
In what byte order one code unit goes out.
Takes the position for the reason WstringLengthPolicy’s
methods do. Measured to agree at the member and the collection
element in all nine implementations.