Search code examples
c++arraysstructarp

C++ variable length arrays in struct


I am writing a program for creating, sending, receiving and interpreting ARP packets. I have a structure representing the ARP header like this:

struct ArpHeader
{
    unsigned short hardwareType;
    unsigned short protocolType;
    unsigned char hardwareAddressLength;
    unsigned char protocolAddressLength;
    unsigned short operationCode;
    unsigned char senderHardwareAddress[6];
    unsigned char senderProtocolAddress[4];
    unsigned char targetHardwareAddress[6];
    unsigned char targetProtocolAddress[4];
};

This only works for hardware addresses with length 6 and protocol addresses with length 4. The address lengths are given in the header as well, so to be correct the structure would have to look something like this:

struct ArpHeader
{
    unsigned short hardwareType;
    unsigned short protocolType;
    unsigned char hardwareAddressLength;
    unsigned char protocolAddressLength;
    unsigned short operationCode;
    unsigned char senderHardwareAddress[hardwareAddressLength];
    unsigned char senderProtocolAddress[protocolAddressLength];
    unsigned char targetHardwareAddress[hardwareAddressLength];
    unsigned char targetProtocolAddress[protocolAddressLength];
};

This obviously won't work since the address lengths are not known at compile time. Template structures aren't an option either since I would like to fill in values for the structure and then just cast it from (ArpHeader*) to (char*) in order to get a byte array which can be sent on the network or cast a received byte array from (char*) to (ArpHeader*) in order to interpret it.

One solution would be to create a class with all header fields as member variables, a function to create a byte array representing the ARP header which can be sent on the network and a constructor which would take only a byte array (received on the network) and interpret it by reading all header fields and writing them to the member variables. This is not a nice solution though since it would require a LOT more code.

In contrary a similar structure for a UDP header for example is simple since all header fields are of known constant size. I use

#pragma pack(push, 1)
#pragma pack(pop)

around the structure declaration so that I can actually do a simple C-style cast to get a byte array to be sent on the network.

Is there any solution I could use here which would be close to a structure or at least not require a lot more code than a structure? I know the last field in a structure (if it is an array) does not need a specific compile-time size, can I use something similar like that for my problem? Just leaving the sizes of those 4 arrays empty will compile, but I have no idea how that would actually function. Just logically speaking it cannot work since the compiler would have no idea where the second array starts if the size of the first array is unknown.


Solution

  • You want a fairly low level thing, an ARP packet, and you are trying to find a way to define a datastructure properly so you can cast the blob into that structure. Instead, you can use an interface over the blob.

    struct ArpHeader {
        mutable std::vector<uint8_t> buf_;
    
        template <typename T>
        struct ref {
            uint8_t * const p_;
            ref (uint8_t *p) : p_(p) {}
            operator T () const { T t; memcpy(&t, p_, sizeof(t)); return t; }
            T operator = (T t) const { memcpy(p_, &t, sizeof(t)); return t; }
        };
    
        template <typename T>
        ref<T> get (size_t offset) const {
            if (offset + sizeof(T) > buf_.size()) throw SOMETHING;
            return ref<T>(&buf_[0] + offset);
        }
    
        ref<uint16_t> hwType() const { return get<uint16_t>(0); }
        ref<uint16_t> protType () const { return get<uint16_t>(2); }
        ref<uint8_t> hwAddrLen () const { return get<uint8_t>(4); }
        ref<uint8_t> protAddrLen () const { return get<uint8_t>(5); }
        ref<uint16_t> opCode () const { return get<uint16_t>(6); }
    
        uint8_t *senderHwAddr () const { return &buf_[0] + 8; }
        uint8_t *senderProtAddr () const { return senderHwAddr() + hwAddrLen(); }
        uint8_t *targetHwAddr () const { return senderProtAddr() + protAddrLen(); }
        uint8_t *targetProtAddr () const { return targetHwAddr() + hwAddrLen(); }
    };
    

    If you need const correctness, you remove mutable, create a const_ref, and duplicate the accessors into non-const versions, and make the const versions return const_ref and const uint8_t *.