For general information on ATDNA, please read the overview article.

Defining a DNA Record

atdna will only emit code for C++ records (structs, classes, unions) that subclass (directly or indirectly) athena::io::DNA<athena::Endian>. The DNA base-class is defined in AthenaCore and can be included using <athena/DNA.hpp>. A master byte-order (endianness) must be selected at this time (athena::BigEndian or athena::LittleEndian).

In addition to the inheritance, the DNA::read() and DNA::write() methods must be declared within the subclass. A convenience macro DECL_DNA is provided to take care of this automatically.

#include <athena/DNA.hpp>
struct MyFirstDNA : public athena::io::DNA<athena::BigEndian>
{
    DECL_DNA
    Value<atUint32> myInt;
};

Adding DNA Fields

atdna uses C++11 type alias template specializations to define DNA layouts. These templates are as follows:

Value

Value<typename type, athena::Endian endian = Inherit> myValue;

Simple numeric primitive type in Athena. Any built-in integer or floating-point type may be provided for the type argument.

Athena uses the following, although atdna will work with built-in C types and <stdint.h> types as well:

Type Description
atInt8 Signed 8-bit (char)
atUint8 Unsigned 8-bit (unsigned char)
atInt16 Signed 16-bit (short)
atUint16 Unsigned 16-bit (unsigned short)
atInt32 Signed 32-bit (long)
atUint32 Unsigned 32-bit (unsigned long)
atInt64 Signed 64-bit (long long)
atUint64 Unsigned 64-bit (unsigned long long)
bool Standard boolean type
float 32-bit IEEE 754 floating-point
double 64-bit IEEE 754 floating-point
atVec3f 3-component 32-bit IEEE 754 floating-point
atVec4f 4-component 32-bit IEEE 754 floating-point

Vector

Vector<typename type, size_t countVar, athena::Endian endian = Inherit> myVector;

This is a wrapper template around std::vector which represents a contiguous table of records. All standard vector operations are available through this type.

The type argument determines the per-element data type used in the vector. It must be a type compatible with Value or another DNA-record subclass.

The countVar argument statically-evaluates the contents of the special macro DNA_COUNT. This macro captures a C expression which is evaluated to determine how many elements the vector will be filled with.

Example

struct VectorDemo : public athena::io::DNA<athena::BigEndian>
{
    DECL_DNA
    Value<atUint32> count;
    
    /* I can also be defined out-of-line */
    struct VectorElement : public athena::io::DNA<athena::BigEndian>
    {
        DECL_DNA
        Value<float> val1;
        Value<float> val2;
    };
    Vector<VectorElement, DNA_COUNT(count)> vector;
};

Buffer

Buffer<size_t sizeVar> myBuffer;

General-purpose data-buffer. The Buffer template is implemented as an atUint8 variable-length buffer wrapped in std::unique_ptr.

Like Vector, the sizeVar works via DNA_COUNT to determine how many bytes long the buffer should read and write.

Example

struct BufferDemo : public athena::io::DNA<athena::BigEndian>
{
    DECL_DNA
    Value<atUint32> length;
    Buffer<DNA_COUNT(length)> buffer;
};

String

String<size_t sizeVar = -1> myString;

General-purpose byte-character string. The String template is implemented as a std::string with all available operations.

By default, strings are assumed to be null-terminated (size -1). If the string is actually fixed-length, the sizeVar argument can be supplied with an integer literal counting the string-buffer’s characters.

WString

WString<size_t sizeVar = -1, athena::Endian endian = Inherit> myWString;

General-purpose wide-character string. The WString template is implemented as a std::wstring with all available operations.

By default, strings are assumed to be null-terminated (size -1). If the string is actually fixed-length, the sizeVar argument can be supplied with an integer literal counting the string-buffer’s characters.

UTF8

UTF8<size_t sizeVar = -1, athena::Endian endian = Inherit> myUTF8;

General-purpose multi-byte-character string. The UTF8 template is implemented as a std::string with all available operations.

By default, strings are assumed to be null-terminated (size -1). If the string is actually fixed-length, the sizeVar argument can be supplied with an integer literal counting the string-buffer’s bytes.

Adding DNA Meta-Fields

atdna provides more than data fields. DNA can also include meta-directives for fine-grained control of the stream.

Seek

Seek<off_t offset, athena::SeekOrigin direction> mySeek;

Not all formats are easily read sequentially from start-to-finish. Sometimes some random-access is required to handle complex, variable-length formats.

The Seek template is provided to move the stream cursor. It works exactly like fseek, supplied with a count of bytes and a direction to seek relative to.

The offset argument works with the DNA_COUNT macro to evaluate a byte-count expression within the record.

The direction argument may be one of the following:

athena::SeekOrigin Description
athena::Begin Offset from start of stream
athena::Current Offset from present stream cursor
athena::End Offset from end of stream

Example

struct SeekDemo : public athena::io::DNA<athena::BigEndian>
{
    DECL_DNA
    Value<atUint32> subRecordOffset;
    Seek<DNA_COUNT(subRecordOffset), athena::Begin> seek1;
    
    /* I start at the absolute subRecordOffset */
    struct SubRecord : public athena::io::DNA<athena::BigEndian>
    {
        DECL_DNA
        Value<float> val1;
        Value<float> val2;
    } record;
};

Align

Align<size_t align> myAlign;

Some data formats are designed to be loaded as a cache-aligned data-blob. In order to maintain cache-alignment, padding-bytes are commonly inserted to bring the start of a sub-record to a 4, 8, 16, 32 byte-aligned position in the stream.

atdna provides the Align template to automatically generate the most efficent cursor-position evaluation; advancing forward to the nearest alignment multiple.

Example

struct AlignDemo : public athena::io::DNA<athena::BigEndian>
{
    DECL_DNA
    struct SubRecordOne : public athena::io::DNA<athena::BigEndian>
    {
        DECL_DNA
        Value<atUint32> val;
    } one;

    Align<32> align1;

    /* I'm 32-byte aligned!! */
    struct SubRecordTwo : public athena::io::DNA<athena::BigEndian>
    {
        DECL_DNA
        Value<atVec3f> val;
    } two;

    Align<32> align2;
    
    /* I'm also 32-byte aligned!! */
    struct SubRecordThree : public athena::io::DNA<athena::BigEndian>
    {
        DECL_DNA
        Value<atUint16> val1;
        Value<atUint16> val2;
    } three;    
};

Delete

Delete myDelete;

Including a single instance of this field in a record prevents atdna from emitting streaming code for the whole record subclass. This is useful for complex types where automatic DNA streaming isn’t practical.

It’s not necessary to use this type directly, the DECL_EXPLICIT_DNA macro automatically places a Delete field in the record. See Writing Explicit Marshalling Functions below for details.

Inheriting DNA Record Types

atdna is fully aware of type-inheritance involving DNA records. Chains of DNA::read() and DNA::write() are automatically inserted for indirectly-inherited DNA subclasses.

Base classes are always visited first, cascading to the individual subclass implementation.

Example

#include <athena/DNA.hpp>

struct BaseDNA : public athena::io::DNA<athena::BigEndian>
{
    DECL_DNA
    Value<atUint32> myInt;
};

struct SubDNA : public BaseDNA
{
    DECL_DNA
    Value<atUint32> mySubInt;
};

struct SubSubDNA : public SubDNA
{
    DECL_DNA
    Value<atUint32> mySubSubInt;
};

void readTest(athena::io::IStreamReader& reader)
{
    SubSubDNA ssDna;
    
    /* This call will run BaseDNA::read(), then SubDNA::read(),
     * then SubSubDNA::read() */
    ssDna.read(reader);
}

Writing Explicit Marshalling Functions

ATDNA is a one size fits many solution. For complex custom data structures like recursive index tables, trees and arbitrarily-chunked-formats, more customized streaming code is required.

The DECL_EXPLICIT_DNA macro is an alternative to DECL_DNA. When an explicit record is defined, atdna will skip emitting streaming code automatically. The resulting program will fail to link, unless the programmer provides replacement DNA::read() and/or DNA::write() implementations.

By convention, the implementation should start by setting the byte order on the reader/writer using IStreamReader::setEndian() or IStreamWriter::setEndian().

Example

#include <athena/DNA.hpp>

struct ExplicitDemo : public athena::io::DNA<athena::BigEndian>
{
    DECL_EXPLICIT_DNA
    Value<atUint32> myInt;
    Value<atUint16> myShort;
};

void ExplicitDemo::read(athena::io::IStreamReader& reader)
{
    reader.setEndian(athena::BigEndian);
    myInt = reader.readUint32();
    myShort = reader.readUint16();
}

void ExplicitDemo::write(athena::io::IStreamWriter& writer) const
{
    writer.setEndian(athena::BigEndian);
    writer.writeUint32(myInt);
    writer.writeUint16(myShort);
}