Context:
When sending message across the network, the sender has to convert the message from host byte order to network byte order and the receiver has to do the the reverse - network order to host order.
There are multiple ways of going about it. One neat way to do is to convert the message to host byte order as soon as it is received.
Eg:
struct Msg {
uint32_t a;
uint16_t b;
};
run_ntoh (Msg *m)
{
m->a = ntohl(m->a);
m->b = ntohs(m->b);
}
process (Msg *m)
{
run_ntoh(m);
further_process(m);
}
But there are cases where this will not work. Say, the message buffer is shared by multiple receivers. We can always copy the message to local buffer and do it. But that is inefficient if the message is large.
The third option is to always access the fields of 'Msg' using ntohx function. But this is error prone.
a. The user might forget to use ntoh always - it is not enforced.
b. Also user has to be very careful - use htonl or htons? What if the size of the varible is changed - nothing enforces the change of htonl instead of htons if the variable size changes from 16 to 32.
Here my idea is to enforce the use of ntoh, while making it easy for the user.
Code:
#include <cstdint>
#include <cstddef>
#include <type_traits>
//
// class to extract N number of bytes and form a number
// uint32_t value = ExtractValue<uint32_t>::get(buffer);
//
template <typename T, size_t val = sizeof(T)>
struct ExtractValue {
static T get (uint8_t *p)
{
return (ExtractValue<T, val - 1>::get(p) << 8) |
static_cast<T>(p[val - 1]);
}
};
template <typename T>
struct ExtractValue<T, 0> {
static T get (uint8_t *p)
{
return 0;
}
};
// Get a field of the class T, in host byte order.
// The class T is wrapped in NetworkData class. The get() function
// takes the offset to the member as an argument, gets the address
// of field and extract the field
template <typename T>
struct NetworkData {
using type = T;
NetworkData (uint8_t *p) : _p(p) {}
template<typename R, typename U = std::decay_t<R>,
typename std::enable_if_t<!std::is_pointer<U>::value>* = nullptr>
U get (uintptr_t offset)
{
return ExtractValue<U>::get(_p + offset);
}
template<typename R, typename U = std::decay_t<R>,
typename std::enable_if_t<std::is_pointer<U>::value>* = nullptr>
U get (uintptr_t offset)
{
return (U)(_p + offset);
}
private:
uint8_t *_p;
};
// Macro to easily call the NetworkData::get()
#define GetNtoh(h, f) \
h.get<decltype(decltype(h)::type::f)>(offsetof(decltype(h)::type, f))
Usage:
struct Msg {
uint32_t a;
uint16_t b;
};
void process (uint8_t *p)
{
NetworkData<Msg> h(p);
auto a = GetNtoh(h, a);
auto b = GetNtoh(h, b);
}
It is always better to use something like goolge protobuf to serialize/deserialize. But that is not possible when the sender is legacy code.
HTONGET()
andNTOHGET()
? \$\endgroup\$hton16
hton32
hton64
? I always though it washtons
andhtonl
? \$\endgroup\$