3
\$\begingroup\$

While doing a little project involving sniffing packets, I found myself programming a cross platform network to host byte order and host to network byte order, this is my first time developing a project using networking.

ntboc.h

#ifndef NTBOC_H_INCLUDED
#define NTBOC_H_INCLUDED

#define LITTLE_EDIAN    1
#define BIG_EDIAN       2

// convert from host to network byte order
void ntboc_tont(void *data, size_t len);
//convert from network to host byte order
#define ntboc_toht(data, len)   (ntboc_tont(data,len))
#endif

ntboc.c

#include <stdlib.h>
#include "ntboc.h"

static unsigned int ntbo = 0;

static void ntboc_init()
{
    int t = 1;
    if(*((char *)&t))
        ntbo = LITTLE_EDIAN;
    else
        ntbo = BIG_EDIAN;
}

static void ntboc_swap(void *data, size_t len)
{
    size_t off = len-1;

    unsigned char *ddata = data;
    unsigned char *sdata = data + off;
    unsigned char tmp;

    while((void *)sdata > (data + (off)/2)) {
        tmp = *ddata;
        *ddata = *sdata;        
        *sdata = tmp;

        sdata--, ddata++;
    }

}

void ntboc_tont(void *data, size_t len) 
{
    if(ntbo == 0)
        ntboc_init();

    if(ntbo == BIG_EDIAN)
        return;

    ntboc_swap(data, len);
}

main()
{
    short len = 0x3e;
    ntboc_toht(&len, 2);
    printf("len : %x", len);
}
\$\endgroup\$
  • 1
    \$\begingroup\$ It's "endian", not "edian". \$\endgroup\$ – ferada Feb 13 '15 at 12:20
4
\$\begingroup\$

The problem with your approach is that your are trying to simultaneously satisfy two contradictory requirements: produce a robust cross-platform implementation, and explicitly deal with different hosts.

A better approach is to avoid any knowledge of the host's endianness by using shifts and or's. It is, however, perfectly fine to rely on network byte order being big-endian as that is an invariant.

Consider the conversion to network order first. This is the simpler direction as it is always fixed but, in C at least, you will need to write routines to deal with the different sizes of int. For now, we will deal with unsigned int only.

void uint_to_network(unsigned int value, unsigned char * buffer, size_t buffer_size)
{
    size_t nw_length = sizeof(value);

    if (buffer_size < nw_length) return; // Avoid potential overflow

    // Note that this loops until all possible locations in the
    // output stream are filled, automatically handling leading zeroes.
    while(nw_length != 0)
    {
        // Pre-decrementing nw_length deals with the zero-based nature
        // of C arrays and avoids us having to worry about the representation
        // of -1 as an unsigned value.
        nw_length--;
        buffer[nw_length] = value & 0xff;
        value >>= 8;
    }
}

Note that this code is written in terms of the network order, with no knowledge of the host's endianness. Thus, it is inherently cross-platform.

Converting from network to host follows a similar pattern.

unsigned int network_to_uint(unsigned char * buffer, size_t input_length)
{
    unsigned int result = 0;
    size_t decode_length;

    decode_length = (input_length > sizeof(unsigned int)) 
                      ? sizeof(unsigned int)
                      : input_length;

    for (size_t i = 0; i < decode_length; i++)
    {
        result <<= 8;
        result |= (unsigned int) buffer[i];
    }

    return result;
}

Again, the code is written in terms of the known network order and makes no reliance on the host order.

Also note that, whenever dealing with network side data, I always use the unsigned char format. This is because network data is basically a 'bit stream', and you should never try to deal with it as a different native data type. That way lies madness .. or at least very subtle bugs.

Now, in a production implementation where execution speed is all important, you would actually have two implementations of the routines. On a big-endian host they would be no-ops, or at worst a macro performing a cast. On a little-endian host, they would be heavily optimized based on the knowledge that conversion was always required. Then your build-script would link in the set of routines that made sense for the target: and you would have some intelligence in the build script if only to select the correct target-processor for the cross compiler.

But for a true cross-platform single-implementation solution, avoiding knowledge of the host's endianness is the only sure route to avoiding problems.

By the way, if you change the return type of network_to_uint to unsigned long long (or unsigned long if your compiler is too old for long long) it can deal with all of the unsigned integer types in the network data by passing in the correct value for input_length. But you will still need a different routine to deal with signed data. And remember that, for an unsigned int in the network data you need to specify the correct input length. A network int may not be the same size as your host's.

For signed data, you have to be a little more careful. There are hosts out there which store data with a separate sign bit (i.e. they can represent both positive and negative zero) and C is designed to run on those as well.

And really, my uint_to_network has the fault that it assumes sizeof(unsigned int) on the host is the same as its representation in a network data stream. It really ought to take an output_length argument instead of setting nw_length to sizeof(unsigned int). Then it can also be made to work for all unsigned data, because it only extracts the least significant bytes of the data.

\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.