What is the safest cross-platform way to get the low byte or the high byte of a 16-bit integer?

426 views Asked by At

While looking at various sdks it seems LOBYTE and HIBYTE are rarely consistent as shown below.

Windows

#define LOBYTE(w)           ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE(w)           ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))

Various Linux Headers

#define HIBYTE(w)       ((u8)(((u16)(w) >> 8) & 0xff))
#define LOBYTE(w)       ((u8)(w))

Why is & 0xff needed if it's cast to a u8? Why wouldn't the following be the way to go? (assuming uint8_t and uint16_t are defined)

#define HIBYTE(w)       ((uint8_t)(((uint16_t)(w) >> 8)))
#define LOBYTE(w)       ((uint8_t)(w))
1

There are 1 answers

1
nilo On BEST ANSWER

From ISO/IEC 9899:TC3, 6.3.1.3 Signed and unsigned integers (under 6.3 Conversions):

  1. When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged.
  2. Otherwise, if the new type is unsigned, the value is converted by repeatedly adding or subtracting one more than the maximum value that can be represented in the new type until the value is in the range of the new type.

While that sounds a little convoluted, it answers the following question.

Why is & 0xff needed if it's cast to a u8?

It is not needed, because the cast does the masking automatically.

When it comes to the question in the topic, the OP's last suggestion is:

#define HIBYTE(w)       ((uint8_t)(((uint16_t)(w) >> 8)))
#define LOBYTE(w)       ((uint8_t)(w))

That will work as expected for all unsigned values. Signed values will always be converted to unsigned values by the macros, which in the case of two's complement will not change the representation, so the results of the calculations are well defined. Assuming two's complement, however, is not portable, so the solution is not strictly portable for signed integers.

Implementing a portable solution for signed integers would be quite difficult, and one could even question the meaning of such an implementation:

  • Is the result supposed to be signed or unsigned?
  • If the result is supposed to be unsigned, it does not really qualify as the high/low byte of the initial number, since a change of representation might be necessary to obtain it.
  • If the result is supposed to signed, it would have to be further specified. The result of >> for negative values, for instance, is implementation-defined, so getting a portable well-defined "high byte" sounds challenging. One should really question the purpose of such a calculation.

And since we are playing language lawyer, we might want to wonder about the signedness of the left operand of (uint16_t)(w) >> 8. Unsigned could seem as the obvious answer, but it is not, because of the integer promotion rules.

Integer promotion applies, among others, to objects or expressions specified as follows.

An object or expression with an integer type whose integer conversion rank is less than or equal to the rank of int and unsigned int.

The integer promotion rule in such a case is specified as:

If an int can represent all values of the original type, the value is converted to an int;

That will be the case for the left operand on a typical 32-bit or 64-bit machine.

Fortunately in such a case, the left operand after conversion will still be nonnegative, which makes the result of >> well defined:

The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / 2E2.