3
\$\begingroup\$

I'm writing some code which needs to be compatible accross a Cortex-M based embedded device and x86 / x86_64 based PCs. I needed a solution for sending binary data between the Cortex-M and the computers. Formerly I was using GCC-specific packed structs for this, but then I realized that it's not portable / reliable enough and came up with the code here.

Requirements

  • No dynamic memory allocations are allowed at all (it is forbidden on the embedded device).
  • There is no need to handle complex serialization scenarios or inheritance.
  • Items that need to be handled are all fundamental types, mostly fixed-size integers like uint32_t or int16_t, etc.
  • Nested packs are allowed as well.
  • Since it only runs on little-endian devices, endianness is not an issue.
  • Portability and reliability are more important than performance.

Rationale

The old code looks something like this:

struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
} __attribute__((__packed__));

struct TelemetryPacket {
    Sensor1Telemetry tele1;
    Sensor2Telemetry tele2;
    // etc...
} __attribute__((__packed__));

However, since this is not guaranteed to work accross different architectures, I came up with the idea to:

  • Store everything at known positions in a byte array
  • Hide away the underlying byte array in a friendly way
  • Fix misalignment issues by using memcpy to and from the byte array

So I'd like to use something like this:

using Sensor1Telemetry = Pack<int16_t, uint32_t, uint16_t>;

Sensor1Telemetry tele;
tele.set<0>(316);
tele.set<1>(42);

The implementation

#include <iostream>
#include <type_traits>
#include <cstdint>
#include <cstring>

namespace detail {

    /**
     * @brief Helps to identify a type.
     *
     * It is typically used by variadic templates to identify
     * a template type parameter inside a variadic template parameter pack.
     */
    template <typename T>
    struct Identify {
        using Type = T;
    };

    /**
     * @brief Chooses the Nth element of a parameter pack.
     *
     * It is typically used by variadic templates to choose
     * the Nth type parameter inside a variadic template parameter pack.
     */
    template <size_t N, typename... T>
    struct Choose;

    template <size_t N, typename T0, typename... T>
    struct Choose<N, T0, T...> : Choose<N - 1, T...> {};

    template <typename T0, typename... T>
    struct Choose<0, T0, T...> : Identify<T0> {};

    /**
     * @brief Calculates the total size of a parameter pack.
     *
     * It is typically used by variadic templates to calculate
     * the total size (in bytes) of all the parameters in a variadic
     * template parameter pack.
     */
    template <typename... TArgs>
    struct SizeSum;

    /**
     * @brief Helper used by `SizeSum` for identification.
     *
     * Used by `SizeSum` to identify one of its specializations.
     * It works on a similar principle as `Identify`.
     */
    template <typename... TArgs>
    struct IdentifySizeSum {
        using Type = SizeSum<TArgs...>;
    };

    /**
     * @brief Helper used by `SizeSum` for choosing.
     *
     * Used by `SizeSum` to choose the one of its specializations
     * with the first N template parameters.
     * It works on a similar principle as `Choose`, but is more complicated.
     */
    template <size_t N, typename... T>
    struct ChooseSizeSum;

    template <size_t N, typename T0, typename... TOthers>
    struct ChooseSizeSum<N, T0, TOthers...> : ChooseSizeSum<N - 1, TOthers...> {};

    template <typename T0, typename... TOthers>
    struct ChooseSizeSum<0, T0, TOthers...> : IdentifySizeSum<T0, TOthers...> {};

    template <typename T0, typename... TOthers>
    struct SizeSum<T0, TOthers...> {
        static constexpr size_t size = sizeof(T0) + SizeSum<TOthers...>::size;
        static constexpr size_t offset = size - sizeof(T0);
    };

    template <typename T>
    struct SizeSum<T> {
        static constexpr size_t size = sizeof(T);
        static constexpr size_t offset = size - sizeof(T);
    };
}

/**
 * @brief Container for packed data.
 *
 * Intended for simple binary protocols for use instead of packed structs.
 */
template <typename... TPackArgs>
class Pack final {

private:
    template <typename T, size_t byteIndex>
    static T getData(const uint8_t *bytes) {
        T result;
        ::std::memcpy(&result, bytes + byteIndex, sizeof(T));
        return result;
    }

    template <typename T, size_t byteIndex>
    static void setData(uint8_t *bytes, const T &item) {
        ::std::memcpy(bytes + byteIndex, &item, sizeof(T));
    }

    template <typename T0>
    static void initializeData(uint8_t *bytes, const T0 &item0) {
        Pack::setData<T0, 0>(bytes, item0);
    }

    template <typename T0, typename... TOthers>
    static void initializeData(uint8_t *bytes, const T0 &item0, const TOthers &... others) {
        Pack::setData<T0, detail::SizeSum<T0, TOthers...>::offset>(bytes, item0);
        Pack::initializeData<TOthers...>(bytes, others...);
    }

    uint8_t bytes_[detail::SizeSum<TPackArgs...>::size];

public:
    template <size_t N>
    using ItemType = typename detail::Choose<N, TPackArgs...>::Type;

    template <size_t N>
    using ItemSize = typename detail::ChooseSizeSum<N, TPackArgs...>::Type;

    explicit Pack() {}

    explicit Pack(const TPackArgs &... args) {
        Pack::initializeData<TPackArgs...>(bytes_, args...);
    }

    void loadBinary(const uint8_t *binary) {
        ::std::memcpy(this->bytes_, binary, sizeof(this->bytes_));
    }

    const uint8_t *binary() const {
        return this->bytes_;
    }

    template <size_t N>
    inline ItemType<N> get() const {
        static_assert(std::is_trivially_copyable<ItemType<N>>::value, "Item type must be trivially copyable.");
        return Pack::getData<ItemType<N>, ItemSize<N>::offset>(bytes_);
    }

    template <size_t N>
    inline void set(const ItemType<N> &item) {
        static_assert(std::is_trivially_copyable<ItemType<N>>::value, "Item type must be trivially copyable.");
        return Pack::setData<ItemType<N>, ItemSize<N>::offset>(bytes_, item);
    }
};

Explanation

I read an article about how std::tuple<> is implemented and it gave me the idea to use variadic templates to do the job.

  • Choose helps to find the correct type parameter
  • SizeSum calculates the size of the parameter pack and the position of each item inside the byte array.
  • Pack brings it all together, providing (almost) tuple-like convenience.

Usage

When a packet arrives, I could do something like this:

TelemetryPacket packet;
packet.loadBinary(receivedBytes);

And then I could just access the data with packet.get<>. When composing a packet, I could use packet.set<> and then send the bytes pointed to by packet.binary() out.

I could also nest them like this:

using TelemetryPacket = Pack<Sensor1Telemetry, Sensor2Telemetry, ... >;

TelemetryPacket packet;
packet.get<0>().set<0>(316);
packet.get<0>().set<1>(42);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.