70 cxSerialise
cxSerialise will be a new header-only library factored out of
cxUtils. It will provide the existing serialisation and
deserialisation support, including archives such as
InputArchive and the variable-length serialisation functions.
The LSS will use cxSerialise generally for its persistent file
format so LSS files are platform-independent. An LSS file created on a little-endian machine can be
read on a big-endian machine, and an LSS file created on a big-endian machine can be read on a
little-endian machine.
Scope
The scope of cxSerialise is not yet fixed. It may be limited to
serialisation to and from binary blobs held in memory. This narrow scope would cover bounded and
unbounded memory archives, the binary wire format and variable-length serialisation, while leaving
stream and paged-storage facilities in cxUtils.
Alternatively, cxSerialise may also include the stream interfaces
currently provided by cxUtils, together with
PagedBuffer and its archive/stream adaptors. This broader scope
would allow serialisation over contiguous memory, streams and paged buffers to be provided by one
library. The choice must preserve the requirement that cxSerialise
has no dependency on cxUtils, Boost or any other external library;
any included stream or paged-buffer code would therefore need to be factored into
cxSerialise with those dependencies removed.
The broader scope might also encompass high-performance checksum support. Checksums are needed by
the LSS to detect corruption in persisted data, and placing checksum support alongside the binary
representation can allow data to be checksummed efficiently as it is serialised, copied or consumed.
The same facility could also be useful for messaging over the wire, where messages may require
integrity checks independently of the transport. The checksum API should work with contiguous binary
blobs and, if streams and PagedBuffer are included, with those
sources and destinations as well.
Dependencies
cxSerialise has no dependencies on any other CEDA library or on
any third-party library, including Boost. In particular, it does not depend on
cxUtils. The dependency is in the other direction:
cxUtils may depend on and use
cxSerialise. Code factored out of
cxUtils must therefore be reorganised to remove its existing
cxUtils and third-party dependencies.
Relevant cxUtils files
The core serialisation files to be factored out are:
Archive.hArchiveCollection.hMemoryAccess.h(the relevant little-endian memory access support)VariableLengthSerialise.h
The following files provide cxUtils-specific serialisation support
or use the archive interfaces. They remain in cxUtils, which can use
cxSerialise, but their includes and dependencies must be updated as
part of the factoring:
ArchiveError.hArchiveHPTime.hArchiveMagic.hBPlusTree.hCrc32.hFile.hGuid.hHalfOpenInterval.hHex.hIMsgOrientedInputStream.hMD5.hPagedBuffer.hPagedBufferAsArchive.hRange.hRelocatableMap.hSessionValueCache.hxostream2.h
Selectable safe and fast archives
The library should provide two implementations of archive types such as
InputArchive. An externally defined preprocessor flag selects
between them when the library headers are included:
// Defined consistently by the consuming project's build configuration.
#define CX_SERIALISE_SAFE_ARCHIVES 1
#include <Ceda/cxSerialise/Archive.h>
- The fast implementation preserves the existing representation and behaviour. It performs no bounds checks and is suitable when the input has already been validated or maximum throughput is required.
-
The safe implementation is constructed from a bounded sequence of octets, preferably a
std::span<const octet_t>, and checks every read, skip and variable-length decode before advancing. Truncated or malformed input produces a well-defined error rather than an out-of-bounds memory access.
Safe InputArchive representation
The safe InputArchive can contain a
std::span<const octet_t> representing all unread input. As
bytes are consumed, the archive advances the start of the span by replacing it with a trailing
subspan. The span's data pointer is therefore the current read position and its size is the number of
octets remaining:
class SafeInputArchive
{
public:
explicit SafeInputArchive(std::span<const octet_t> input) : remaining_(input) {}
std::span<const octet_t> Read(std::size_t n)
{
if (n > remaining_.size())
throw EndOfStreamException();
auto result = remaining_.first(n);
remaining_ = remaining_.subspan(n);
return result;
}
private:
std::span<const octet_t> remaining_;
};
The archive should contain the span rather than publicly inherit from it or be an alias for it. This preserves the archive abstraction, prevents client code from bypassing its checked operations, and allows the representation to change without changing the public serialisation interface.
Using std::span does not by itself make deserialisation safe because
unchecked operator[] can still access past the end. Each operation
must check its complete range before calling first() or
subspan(). This representation avoids maintaining a separate
current pointer and end pointer and avoids unchecked pointer addition when advancing.
Variable-length integers need both input-bound checks and validation of their maximum encoded length and terminal byte. A compound decode should work on a local copy of the remaining span and only commit the advanced span to the archive after the entire value is valid. An error then leaves the archive position unchanged rather than partially consuming malformed input.
Source compatibility
Both implementations expose the same public type names and normal serialisation interface. Client
code continues to use InputArchive,
OutputArchive and the existing
Serialise()/Deserialise()
functions. Flipping the project-wide flag should therefore require virtually no source changes.
Code which already knows the buffer size can use a span constructor in either mode:
InputArchive ar(std::span(buffer));
Deserialise(ar, value);
For migration, the pointer-only constructor can remain available in fast mode. Safe mode cannot make that constructor safe because no bound is supplied; it should either be unavailable in safe mode or require a pointer and size. Making unsafe pointer-only construction a compile-time error in safe mode is preferable, because it identifies the relatively small number of call sites that must be changed to provide the buffer extent. After those boundary call sites use spans, application serialisation code can switch modes without changes.
Implementation selection
The two implementations can have internal names such as
UnsafeInputArchive and
SafeInputArchive, with
InputArchive defined as an alias selected by the flag. Shared
serialisation algorithms should be templates over the archive interface so they are not duplicated.
The flag should have a documented default, while build systems set it explicitly for each consuming
project.
The selection must be consistent across every translation unit and linked library in a program. Changing the flag can change archive layout, inline function definitions and instantiated template types; mixing modes can therefore cause one-definition-rule and ABI errors. The selected mode should be part of the exported build configuration, and a link-time configuration symbol or equivalent guard should be used where practical to make mismatches fail clearly. Data written in either mode must use exactly the same wire format, so the choice affects validation and performance but not persistent or exchanged data.
Platform-independent byte order
cxSerialise must be platform-independent with respect to
little-endian and big-endian processor architectures. The existing
cxUtils serialisation format already specifies little-endian byte
order on the wire: Archive.h uses the
SetUnalignedLE() and
GetUnalignedLE() operations provided by
MemoryAccess.h. cxSerialise
must preserve this established little-endian wire format rather than introduce a new byte order.
The wire order does not depend on the native byte order of the machine producing it. A little-endian machine must be able to serialise data that is then deserialised correctly by a big-endian machine, and a big-endian machine must be able to serialise data that is then deserialised correctly by a little-endian machine.
Serialisation and deserialisation of every multi-octet primitive must perform the necessary native to wire and wire to native byte-order conversion. This applies to integral and floating-point values, fixed-size fields and any length or control fields used by the archive format. The safe and fast archive implementations must use exactly the same platform-independent wire representation. Cross-endian test vectors should verify byte-for-byte output and round-trip compatibility in both directions.
Floating-point serialisation assumes the IEEE 754 interchange formats: binary32 for 32-bit floating-point values and binary64 for 64-bit floating-point values. The IEEE 754 bit pattern is encoded using the byte order defined by the wire format; it must not be written using the machine's native byte order. Implementations should verify the required floating-point representation at compile time. A platform that does not provide compatible IEEE 754 binary32 and binary64 types is not directly supported unless it explicitly converts its native representation to and from these wire formats.
Scope and verification
The same pattern can be applied where bounds matter to output archives and other cursor-like archive types. Safe output archives must check remaining capacity before writing; sizing archives do not access a buffer but must still detect size arithmetic overflow. Tests should run the same serialisation suite in both configurations, verify byte-for-byte identical output, and exercise the safe mode with truncated input, malformed variable-length integers, zero-length buffers and sizes near arithmetic limits. Benchmarks should quantify the cost of safe mode rather than allowing the safe checks to affect the fast implementation.
Documentation and performance benchmarks
cxSerialise will have its own document describing the library,
its wire format, public API, safe and unsafe configurations, error handling, portability guarantees
and recommended usage. That document will include performance benchmarks which give users a good
idea of the performance difference between the safe and unsafe versions.
The benchmarks should exercise representative primitive values, arrays, collections, variable-length integers and larger structured values. They should report serialisation and deserialisation throughput, latency where useful, and the relative overhead of bounds checking. Both versions must be measured using the same data, compiler, optimisation settings and hardware so the comparison is meaningful. The document should record this test environment and enough methodology for the results to be reproduced.