Self-describing binary serialization for PIC.
## Stream layout
Every stream starts with a 16 byte header followed by zero or more records. All multi-byte fields are written in the native byte order of the writing machine; the header carries a byte-order mark so that a reader on a foreign machine can detect this and un-swap on the fly.
### Header, 16 bytes
| offset | size | field | description |
|---|---|---|---|
| 0 | 4 | magic | ASCII "PIC1", byte order independent |
| 4 | 4 | endian mark | integer(int32) 0x01020304 (16909060) |
| 8 | 4 | version | integer(int32) format version |
| 12 | 4 | reserved | integer(int32) written as 0, readers skip it |
There is deliberately no payload-length field in the header. The number and kind of records that follow is decided by the caller, and each record is individually length prefixed and bounds checked against the real size of the file, so a global length would only be a second source of truth that can disagree with the first. The reserved word exists so a future version can add one without changing the header size.
### Record, 12 byte record header plus payload
| offset | size | field | description |
|---|---|---|---|
| 0 | 4 | tag | integer(int32) type tag, see PIC_TAG_* |
| 4 | 8 | count | integer(int64) number of elements |
| 12 | n | payload | count * element size bytes |
count counts elements, not bytes: three real(dp) values give
count == 3 and a 24 byte payload. For a character record count is the
number of characters, so the element size is one.
## Fixed widths on purpose
Every field that reaches the file is an explicit int32/int64, never
default_int. A file written by a build with PIC_DEFAULT_INT8=ON is
byte identical to one written by a default build; the test suite pins the
exact byte length of a known stream so this cannot regress silently.
## Error contract
Every procedure takes type(error_t), intent(inout) :: err and writes it
only on failure; it is never cleared on entry. Pass a fresh or cleared
error_t and check it after each call, which is the convention the rest
of PIC uses. Keeping err intent(inout) rather than intent(out) also
keeps the compiler from emitting a conditional deallocation of the error
message on every single call into this module.
## Usage
type(error_t) :: err
integer(default_int) :: unit
integer(int32) :: version
logical :: swapped
real(dp), allocatable :: values(:)
call serialize_open_write("state.bin", unit, err)
call write_header(unit, err)
call write_array(unit, [1.0_dp, 2.0_dp], err)
call serialize_close(unit, err)
call serialize_open_read("state.bin", unit, err)
call read_header(unit, version, swapped, err)
call read_array(unit, values, swapped, err)
call serialize_close(unit, err)
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer(kind=int32), | public, | parameter | :: | PIC_FORMAT_VERSION | = | 1_int32 |
Current envelope version. Readers accept
|
| integer(kind=int64), | public, | parameter | :: | PIC_HEADER_BYTES | = | 16_int64 |
Size of the stream header in bytes. |
| character(len=*), | public, | parameter | :: | PIC_MAGIC | = | "PIC1" |
Four byte ASCII magic written at offset 0 of every stream. Being characters it has no byte order of its own, which is what makes it usable to reject a foreign file before anything else is decoded. |
| integer(kind=int64), | public, | parameter | :: | PIC_RECORD_HEADER_BYTES | = | 12_int64 |
Size of one record header, tag plus element count, in bytes. |
| integer(kind=int32), | public, | parameter | :: | PIC_TAG_CHAR | = | 5_int32 |
Record tag for a character scalar, 1 byte per element. |
| integer(kind=int32), | public, | parameter | :: | PIC_TAG_INT32 | = | 1_int32 |
Record tag for |
| integer(kind=int32), | public, | parameter | :: | PIC_TAG_INT64 | = | 2_int32 |
Record tag for |
| integer(kind=int32), | public, | parameter | :: | PIC_TAG_REAL_DP | = | 4_int32 |
Record tag for |
| integer(kind=int32), | public, | parameter | :: | PIC_TAG_REAL_SP | = | 3_int32 |
Record tag for |
| integer(kind=int32), | private, | parameter | :: | BITS_PER_BYTE | = | 8_int32 |
Width of one byte field, used by the byte reversal helpers. |
| integer(kind=int32), | private, | parameter | :: | PIC_ENDIAN_MARK | = | 16909060_int32 |
|
| integer(kind=int32), | private, | parameter | :: | RESERVED_WORD | = | 0_int32 |
Value written into the reserved header slot. |
Read one record back, validating its type tag and its length.
Usage: call read_array(unit, values, swapped, err)
values is allocatable and is allocated to the length stored in the
record. swapped is the flag returned by read_header; when it is
true every multi-byte element is byte reversed after reading.
Fails with ERROR_VALIDATION if the record holds a different type
than the one requested, and with ERROR_IO if the payload announced
by the record header runs past the end of the file.
Read an integer(int32) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int32), | intent(out), | allocatable | :: | values(:) | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read an integer(int64) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int64), | intent(out), | allocatable | :: | values(:) | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read a real(sp) array record.
On the native path the values are read straight into values. On the
foreign endian path the same bytes are read into an int32 buffer,
reversed with integer arithmetic, and only then reinterpreted.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| real(kind=sp), | intent(out), | allocatable | :: | values(:) | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read a real(dp) array record. See read_real_sp_array for the way
the foreign endian path is kept away from the native one.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| real(kind=dp), | intent(out), | allocatable | :: | values(:) | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read a character scalar record into a deferred length string.
Characters are single bytes so swapped never changes the payload,
but it is still needed to decode the record header itself.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| character(len=:), | intent(out), | allocatable | :: | text | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Write one length prefixed, type tagged record to a stream unit.
Usage: call write_array(unit, values, err)
The unit must have been opened with access='stream' and
form='unformatted', for instance by serialize_open_write, and
write_header must already have been called on it. Zero sized arrays
and zero length strings are legal and produce a record header with
count == 0 and no payload.
Write an integer(int32) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int32), | intent(in) | :: | values(:) | |||
| type(error_t), | intent(inout) | :: | err |
Write an integer(int64) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int64), | intent(in) | :: | values(:) | |||
| type(error_t), | intent(inout) | :: | err |
Write a real(sp) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| real(kind=sp), | intent(in) | :: | values(:) | |||
| type(error_t), | intent(inout) | :: | err |
Write a real(dp) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| real(kind=dp), | intent(in) | :: | values(:) | |||
| type(error_t), | intent(inout) | :: | err |
Write a character scalar record. The stored count is the number of
characters, so a record of n characters occupies n payload bytes.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| character(len=*), | intent(in) | :: | text | |||
| type(error_t), | intent(inout) | :: | err |
Report whether the running machine stores the least significant byte of an integer first.
Total on disk size of a record, including its 12 byte record header.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | tag |
One of the |
||
| integer(kind=int64), | intent(in) | :: | count |
Number of elements in the record. |
Reinterpret the bit pattern of an int64 as a real(dp).
See bits_to_real_sp for why this is confined to the swapped path.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | bits |
Reinterpret the bit pattern of an int32 as a real(sp).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | bits |
Reverse the four bytes of an int32.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | x |
Reverse the eight bytes of an int64.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int64), | intent(in) | :: | x |
Bytes occupied by one element of the record type tag, or -1 for an
unknown tag.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=int32), | intent(in) | :: | tag |
Read and validate the stream header.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit |
Stream unit opened for unformatted stream reading. |
||
| integer(kind=int32), | intent(out) | :: | version |
Format version found in the file, 0 if the header was rejected. |
||
| logical, | intent(out) | :: | swapped |
True when the file was written by a machine of the opposite byte order, so every multi-byte field needs reversing on the way in. |
||
| type(error_t), | intent(inout) | :: | err |
Error state. |
Close a unit previously opened by one of the open helpers.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit |
Unit to close. |
||
| type(error_t), | intent(inout) | :: | err |
Error state, |
Open an existing serialized stream for reading.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| character(len=*), | intent(in) | :: | filename |
Path of the file to read. |
||
| integer(kind=default_int), | intent(out) | :: | unit |
Newly opened unit number, -1 when the open failed. |
||
| type(error_t), | intent(inout) | :: | err |
Error state, |
Open filename for writing a serialized stream, replacing any
existing file.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| character(len=*), | intent(in) | :: | filename |
Path of the file to create. |
||
| integer(kind=default_int), | intent(out) | :: | unit |
Newly opened unit number, -1 when the open failed. |
||
| type(error_t), | intent(inout) | :: | err |
Error state, |
Write the 16 byte stream header at the current position.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit |
Stream unit opened for unformatted stream writing. |
||
| type(error_t), | intent(inout) | :: | err |
Error state, |
Turn a non zero iostat into an ERROR_IO carrying the raw value.
Every I/O statement in this module funnels its status through here so
that the failure message always names both the operation and the
iostat the runtime produced.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | ios | |||
| character(len=*), | intent(in) | :: | context | |||
| type(error_t), | intent(inout) | :: | err |
Read a character scalar record into a deferred length string.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| character(len=:), | intent(out), | allocatable | :: | text | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read an integer(int32) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int32), | intent(out), | allocatable | :: | values(:) | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read an integer(int64) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int64), | intent(out), | allocatable | :: | values(:) | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read a real(dp) array record. See read_real_sp_array for the way
the foreign endian path is kept away from the native one.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| real(kind=dp), | intent(out), | allocatable | :: | values(:) | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read a real(sp) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| real(kind=sp), | intent(out), | allocatable | :: | values(:) | ||
| logical, | intent(in) | :: | swapped | |||
| type(error_t), | intent(inout) | :: | err |
Read a record header, check the type tag, and bounds check the announced payload against the real length of the file.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int32), | intent(in) | :: | expected_tag | |||
| logical, | intent(in) | :: | swapped | |||
| integer(kind=int64), | intent(out) | :: | count | |||
| logical, | intent(out) | :: | ok | |||
| type(error_t), | intent(inout) | :: | err |
Write a character scalar record. The stored count is the number of
characters, so a record of n characters occupies n payload bytes.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| character(len=*), | intent(in) | :: | text | |||
| type(error_t), | intent(inout) | :: | err |
Write an integer(int32) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int32), | intent(in) | :: | values(:) | |||
| type(error_t), | intent(inout) | :: | err |
Write an integer(int64) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int64), | intent(in) | :: | values(:) | |||
| type(error_t), | intent(inout) | :: | err |
Write a real(dp) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| real(kind=dp), | intent(in) | :: | values(:) | |||
| type(error_t), | intent(inout) | :: | err |
Write a real(sp) array record.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| real(kind=sp), | intent(in) | :: | values(:) | |||
| type(error_t), | intent(inout) | :: | err |
Write a record header: type tag followed by the element count.
ok rather than err carries the outcome so that a stale error left
in err by an earlier call cannot be mistaken for a failure here.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer(kind=default_int), | intent(in) | :: | unit | |||
| integer(kind=int32), | intent(in) | :: | tag | |||
| integer(kind=int64), | intent(in) | :: | count | |||
| logical, | intent(out) | :: | ok | |||
| type(error_t), | intent(inout) | :: | err |