I'm starting with implementing a TCP/IP stack for embedded systems which will be done in C++ and I've found I need a good way to work with protocol headers involved (e.g. ARP, IP, TCP). Major requirements are:
- Portable, C++(11) standard compliant (e.g. no structure packing, independent of CPU endianness).
- Completely correct (e.g. no potential strict-aliasing violations).
- Minimal boilerplate for both definition of headers and access.
- Ability to read and write fields of headers in arbitrary memory locations (no need to work on a copy of data).
- Efficient (e.g. no runtime metainformation about headers).
I've produced a solution which relies heavily on template metaprogramming. The code is below, it's quite some number of lines, though much of it is comments. The full code along with required headers can be found in my open-source project here.
Below is also some example code which demonstrates how it can be used.
I'd like to get some feedback about how to make it better, in any respect. For example to make usage less verbose; currently, it still needs a bit more typing than "pointer->field". Or any ideas about the interactions between the Val, Ref and ConstRef classes.
Code:
/**
* This class is specialized for different field types
* to define type-specific behavior, e.g. how to
* encode a logical value into bytes and how to decode
* bytes into a value.
*/
template <typename Type, typename Dummy=void>
struct StructTypeHandler;
#define APRINTER_STRUCT_REGISTER_TYPE(Type, TypeHandler) \
template <> \
struct StructTypeHandler<Type, void> { \
using Handler = TypeHandler; \
};
template <typename TType>
struct StructField {
using Type = TType;
};
/**
* Base class for protocol structure definitions.
*
* Notable features of the system are:
* - Automatic endianness handling (big endian encoding is used).
* That is, the user always interacts with logical values while
* the framework manages the byte-level representaion.
* - Can reference structures in existing memory (no need for pointer
* casts violatign strict aliasing).
* - Support for nested structures.
* - Ability to define custom field types.
*
* Structures should be defined through the APRINTER_TSTRUCT macro which
* results in a struct type inheriting StructBase. Example:
*
* \code
* APRINTER_TSTRUCT(MyHeader,
* (FieldA, uint32_t)
* (FieldB, uint64_t)
* )
* \endcode
*
* Each field specified will result in a type within this structure that
* is used as an identifier for the field (e.g. MyHeader::FieldA).
*
* There will be three classes within the main structure providing
* different ways to work with structure data:
* - Val: contains structure data as char[].
* - Ref: references structure data using char *.
* - ConstRef: references structure data using char const *.
*
* Note that the structure type itself (MyHeader) has no runtime use, it
* is a zero-sized structure.
*
* Note, internally APRINTER_TSTRUCT will expand to something like this:
*
* struct MyHeader : public StructBase\<MyHeader\> {
* struct FieldA : public StructField\<uint32_t\>;
* struct FieldB : public StructField\<uint64_t\>;
* using StructFields = MakeTypeList\<FieldA, FieldB\>;
* };
*/
template <typename TStructType>
class StructBase {
private:
using StructType = TStructType;
template <typename This=StructBase>
using Fields = typename This::StructType::StructFields;
template <int FieldIndex, typename Dummy=void>
struct FieldInfo;
template <typename Dummy>
struct FieldInfo<-1, Dummy> {
static size_t const StructSize = 0;
};
template <int FieldIndex, typename Dummy>
struct FieldInfo {
using PrevFieldInfo = FieldInfo<FieldIndex-1, void>;
using Field = TypeListGet<Fields<>, FieldIndex>;
using Handler = typename StructTypeHandler<typename Field::Type>::Handler;
using ValType = typename Handler::ValType;
static size_t const FieldOffset = PrevFieldInfo::StructSize;
static size_t const FieldSize = Handler::FieldSize;
static size_t const StructSize = FieldOffset + FieldSize;
};
template <typename Field, typename This=StructBase>
using GetFieldInfo = FieldInfo<TypeListIndex<Fields<This>, Field>::Value, void>;
template <typename This=StructBase>
using LastFieldInfo = FieldInfo<TypeListLength<Fields<This>>::Value-1, void>;
public:
class Ref;
class ConstRef;
class Val;
/**
* Gets the value type of a specific field.
*
* Example: ValType\<MyHeader::FieldA\> is uint32_t.
*
* @tparam Field field identifier
*/
template <typename Field>
using ValType = typename StructTypeHandler<typename Field::Type>::Handler::ValType;
/**
* Gets the reference type of a specific field.
* Support for this depends on the type handler (e.g. nested structures).
*/
template <typename Field>
using RefType = typename StructTypeHandler<typename Field::Type>::Handler::RefType;
/**
* Gets the const-reference type of a specific field.
* Support for this depends on the type handler (e.g. nested structures).
*/
template <typename Field>
using ConstRefType = typename StructTypeHandler<typename Field::Type>::Handler::ConstRefType;
/**
* Returns the size of the structure.
*/
inline static constexpr size_t Size ()
{
return LastFieldInfo<>::StructSize;
}
/**
* Reads a field.
*
* @tparam Field field identifier
* @param data pointer to the start of a structure
* @return field value that was read
*/
template <typename Field>
inline static ValType<Field> get (char const *data, Field)
{
using Info = GetFieldInfo<Field>;
return Info::Handler::get(data + Info::FieldOffset);
}
/**
* Writes a field.
*
* @tparam Field field identifier
* @param data pointer to the start of a structure
* @param value field value to set
*/
template <typename Field>
inline static void set (char *data, Field, ValType<Field> value)
{
using Info = GetFieldInfo<Field>;
Info::Handler::set(data + Info::FieldOffset, value);
}
/**
* Returns a reference to a field.
* Support for this depends on the type handler (e.g. nested structures).
*/
template <typename Field>
inline static RefType<Field> ref (char *data, Field)
{
using Info = GetFieldInfo<Field>;
return Info::Handler::ref(data + Info::FieldOffset);
}
/**
* Returns a const reference to a field.
* Support for this depends on the type handler (e.g. nested structures).
*/
template <typename Field>
inline static ConstRefType<Field> ref (char const *data, Field)
{
using Info = GetFieldInfo<Field>;
return Info::Handler::const_ref(data + Info::FieldOffset);
}
/**
* Returns a Ref class referencing the specified memory.
*
* @param data pointer to the start of a structure
*/
inline static Ref MakeRef (char *data)
{
return Ref{data};
}
/**
* Returns a ConstRef class referencing the specified memory.
*
* @param data pointer to the start of a structure
*/
inline static ConstRef MakeRef (char const *data)
{
return ConstRef{data};
}
/**
* Reads a structure from the specified memory location and
* returns a Val class containing the structure data.
*
* @param data pointer to the start of a structure
* @return a Val class initialized with a copy of the data
*/
inline static Val MakeVal (char const *data)
{
Val val;
memcpy(val.data, data, Size());
return val;
}
/**
* Class which contains structure data.
* These can be created using StructBase::MakeVal or from
* the Val conversion operators in Ref and ConstRef.
*/
class Val {
public:
using Struct = StructType;
/**
* Reads a field.
* @see StructBase::get
*/
template <typename Field>
inline ValType<Field> get (Field) const
{
return StructBase::get(data, Field());
}
/**
* Writes a field.
* @see StructBase::set
*/
template <typename Field>
inline void set (Field, ValType<Field> value)
{
StructBase::set(data, Field(), value);
}
/**
* Returns a reference to a field.
* @see StructBase::ref
*/
template <typename Field>
inline RefType<Field> ref (Field)
{
return StructBase::ref(data, Field());
}
/**
* Returns a const reference to a field.
* @see StructBase::ref
*/
template <typename Field>
inline ConstRefType<Field> ref (Field) const
{
return StructBase::ref(data, Field());
}
/**
* Returns a Ref referencing this Val.
*/
inline operator Ref ()
{
return Ref{data};
}
/**
* Returns a ConstRef referencing this Val.
*/
inline operator ConstRef () const
{
return ConstRef{data};
}
public:
/**
* The data array.
*/
char data[LastFieldInfo<>::StructSize];
};
/**
* Structure access class referencing external data via
* char *.
* Can be initialized via StructBase::MakeRef or Ref{data}.
*/
class Ref {
public:
using Struct = StructType;
/**
* Reads a field.
* @see StructBase::get
*/
template <typename Field>
inline ValType<Field> get (Field) const
{
return StructBase::get(data, Field());
}
/**
* Writes a field.
* @see StructBase::set
*/
template <typename Field>
inline void set (Field, ValType<Field> value) const
{
StructBase::set(data, Field(), value);
}
/**
* Returns a reference to a field.
* @see StructBase::ref
*/
template <typename Field>
inline RefType<Field> ref (Field) const
{
return StructBase::ref(data, Field());
}
/**
* Returns a ConstRef referencing the same memory.
*/
inline operator ConstRef () const
{
return ConstRef{data};
}
/**
* Reads and returns the current structure data as a Val.
*/
inline operator Val () const
{
return MakeVal(data);
}
/**
* Copies the structure referenced by a ConstRef
* over the structure referenced by this Ref.
* Note: uses memcpy, so don't use with self.
*/
inline void load (ConstRef src) const
{
memcpy(data, src.data, Size());
}
public:
char *data;
};
/**
* Structure access class referencing external data via
* char const *.
* Can be initialized via StructBase::MakeRef or ConstRef{data}.
*/
class ConstRef {
public:
using Struct = StructType;
/**
* Reads a field.
* @see StructBase::get
*/
template <typename Field>
inline ValType<Field> get (Field) const
{
return StructBase::get(data, Field());
}
/**
* Returns a const reference to a field.
* @see StructBase::ref
*/
template <typename Field>
inline ConstRefType<Field> ref (Field) const
{
return StructBase::ref(data, Field());
}
/**
* Reads and returns the current structure data as a Val.
*/
inline operator Val () const
{
return MakeVal(data);
}
public:
char const *data;
};
};
/**
* Macro for defining structures.
* @see StructBase
*/
#define APRINTER_TSTRUCT(StructName, Fields) \
struct StructName : public APrinter::StructBase<StructName> { \
APRINTER_TSTRUCT__ADD_END(APRINTER_TSTRUCT__FIELD_1 Fields) \
using StructFields = APrinter::MakeTypeList< \
APRINTER_TSTRUCT__ADD_END(APRINTER_TSTRUCT__LIST_0 Fields) \
>; \
};
#define APRINTER_TSTRUCT__ADD_END(...) APRINTER_TSTRUCT__ADD_END_2(__VA_ARGS__)
#define APRINTER_TSTRUCT__ADD_END_2(...) __VA_ARGS__ ## _END
#define APRINTER_TSTRUCT__FIELD_1(FieldName, FieldType) \
struct FieldName : public APrinter::StructField<FieldType> {}; \
APRINTER_TSTRUCT__FIELD_2
#define APRINTER_TSTRUCT__FIELD_2(FieldName, FieldType) \
struct FieldName : public APrinter::StructField<FieldType> {}; \
APRINTER_TSTRUCT__FIELD_1
#define APRINTER_TSTRUCT__FIELD_1_END
#define APRINTER_TSTRUCT__FIELD_2_END
#define APRINTER_TSTRUCT__LIST_0(FieldName, FieldType) FieldName APRINTER_TSTRUCT__LIST_1
#define APRINTER_TSTRUCT__LIST_1(FieldName, FieldType) , FieldName APRINTER_TSTRUCT__LIST_2
#define APRINTER_TSTRUCT__LIST_2(FieldName, FieldType) , FieldName APRINTER_TSTRUCT__LIST_1
#define APRINTER_TSTRUCT__LIST_1_END
#define APRINTER_TSTRUCT__LIST_2_END
/**
* Structure field type handler for integer types using
* big-endian representaion.
* These type handlers are registered by default for signed and
* unsigned fixed-width types: intN_t and uintN_t (N=8,16,32,64).
*
* Relies on ReadBinaryInt and WriteBinaryInt.
*/
template <typename Type>
struct StructBinaryTypeHandler {
static size_t const FieldSize = sizeof(Type);
using ValType = Type;
inline static ValType get (char const *data)
{
return ReadBinaryInt<Type, BinaryBigEndian>(data);
}
inline static void set (char *data, ValType value)
{
WriteBinaryInt<Type, BinaryBigEndian>(value, data);
}
};
#define APRINTER_STRUCT_REGISTER_BINARY_TYPE(Type) \
APRINTER_STRUCT_REGISTER_TYPE(Type, StructBinaryTypeHandler<Type>)
APRINTER_STRUCT_REGISTER_BINARY_TYPE(uint8_t)
APRINTER_STRUCT_REGISTER_BINARY_TYPE(uint16_t)
APRINTER_STRUCT_REGISTER_BINARY_TYPE(uint32_t)
APRINTER_STRUCT_REGISTER_BINARY_TYPE(uint64_t)
APRINTER_STRUCT_REGISTER_BINARY_TYPE(int8_t)
APRINTER_STRUCT_REGISTER_BINARY_TYPE(int16_t)
APRINTER_STRUCT_REGISTER_BINARY_TYPE(int32_t)
APRINTER_STRUCT_REGISTER_BINARY_TYPE(int64_t)
/**
* Type handler for structure types, allowing nesting of structures.
*
* It provides:
* - get() and set() operations using StructType::Val.
* - ref() operations using StructType::Ref and StructType::ConstRef.
*/
template <typename StructType>
struct StructNestedTypeHandler {
static size_t const FieldSize = StructType::Size();
using ValType = typename StructType::Val;
using RefType = typename StructType::Ref;
using ConstRefType = typename StructType::ConstRef;
inline static ValType get (char const *data)
{
return StructType::MakeVal(data);
}
inline static void set (char *data, ValType value)
{
memcpy(data, value.data, sizeof(value.data));
}
inline static RefType ref (char *data)
{
return RefType{data};
}
inline static ConstRefType const_ref (char const *data)
{
return ConstRefType{data};
}
};
template <typename Type>
struct StructTypeHandler<Type, EnableIf<__is_base_of(StructBase<Type>, Type), void>> {
using Handler = StructNestedTypeHandler<Type>;
};
Example code:
APRINTER_TSTRUCT(HeaderFoo,
(FieldA, int8_t)
(FieldB, int64_t)
)
APRINTER_TSTRUCT(HeaderBar,
(FieldC, int8_t)
(FieldD, uint32_t)
(FieldFoo, HeaderFoo)
)
void main ()
{
// Create a FooHeader::Val (a type which contains data), set field values.
HeaderFoo::Val foo;
foo.set(HeaderFoo::FieldA(), 30);
foo.set(HeaderFoo::FieldB(), -55);
// Change it via FooHeader::Ref (a type which references data).
HeaderFoo::Ref foo_ref = foo;
foo_ref.set(HeaderFoo::FieldA(), 61);
// Get values via FooHeader::ConstRef (a type which references const data).
// Note: Val, Ref and ConstRef all suppport get().
HeaderFoo::ConstRef foo_cref = foo;
printf("%" PRIi8 " %" PRIi64 "\n",
foo_cref.get(HeaderFoo::FieldA()),
foo_cref.get(HeaderFoo::FieldB()));
// Allocate memory for a HeaderBar as char[] and initialize
// parts of it through HeaderBar::Ref.
char bar_mem[HeaderBar::Size()];
HeaderBar::Ref bar_ref = HeaderBar::MakeRef(bar_mem);
bar_ref.set(HeaderBar::FieldC(), -75);
bar_ref.set(HeaderBar::FieldD(), 70000);
// Initialize the nested HeaderFoo from foo.
// This goes like this:
// - Get a reference to the contained HeaderFoo via .ref(),
// obtaining a HeaderFoo::Ref.
// - Call load() on the HeaderFoo::Ref to copy data from a
// HeaderFoo::ConstRef, which is created from HeaderFoo::Val
// automatically by a conversion operator.
bar_ref.ref(HeaderBar::FieldFoo()).load(foo);
// Get the nested HeaderFoo from bar_ref as a value.
// This will be a HeaderFoo::Val.
// Change the original to prove it's a copy.
auto foo_copy = bar_ref.get(HeaderBar::FieldFoo());
bar_ref.ref(HeaderBar::FieldFoo()).set(HeaderFoo::FieldA(), 4);
printf("%" PRIi8 " %" PRIi8 "\n",
bar_ref.ref(HeaderBar::FieldFoo()).get(HeaderFoo::FieldA()),
foo_copy.get(HeaderFoo::FieldA()));
}