-1

I am trying to use bitwise operators to convert four 16-bit integers into a single 64-bit integer in C (OS is Linux running Ubuntu 20.04 Server). My overall goal is to be able to do this with four 32-bit integers into a single 128-bit integer (an IPv6 source IP address from the four 32-bit integers inside the in6_addr struct). However, in my example below, I am using four 16-bit integers and a single 64-bit integer since I've had issues assigning 128-bit integers using GCC (I'd assume I can apply the same logic from below to my overall goal).

My system's byte order is also in little endian in this case. However, if you'd like to show how to do this on big endian as well, feel free to do so!

Here's my current testing code in C:

#include <stdio.h>
#include <inttypes.h>

int main()
{
    uint16_t nums[4];
    uint64_t num = 2310051230312123321;
    uint64_t mainnum = 0;

    #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
        nums[0] = num >> 0;     // xxxx ---- ---- ----
        nums[1] = num >> 16;    // ---- ---- ---- xxxx
        nums[2] = num >> 32;    // ---- ---- xxxx ----
        nums[3] = num >> 48;    // ---- xxxx ---- ----
    #else
        /*
        nums[0] = num << 0;
        nums[1] = num << 16;
        nums[2] = num << 32;
        nums[3] = num << 48;
        */
    #endif

    printf("Num => %" PRIu64 "\nNum #1 => %" PRIu16 "\nNum #2 => %" PRIu16 "\nNum #3 => %" PRIu16 "\nNum #4 => %" PRIu16 "\n\n", num, nums[0], nums[1], nums[2], nums[3]);

    #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
        mainnum = ((uint64_t)(nums[0] >> 16 | nums[1]) >> 48) | (nums[3] << 16 | nums[2]);
    #else
        /* ... */
    #endif

    uint16_t other[4];

    other[0] = mainnum >> 0;
    other[1] = mainnum >> 16;
    other[2] = mainnum >> 32;
    other[3] = mainnum >> 48;

    printf("Num => %" PRIu64 "\nNum #1 => %" PRIu16 "\nNum #2 => %" PRIu16 "\nNum #3 => %" PRIu16 "\nNum #4 => %" PRIu16 "\n", mainnum, other[0], other[1], other[2], other[3]);

    return 0;
}

The program outputs the following:

Num => 2310051230312123321
Num #1 => 19385
Num #2 => 54197
Num #3 => 62298
Num #4 => 8206

Num => 537850714
Num #1 => 62298
Num #2 => 8206
Num #3 => 0
Num #4 => 0

I want num and mainnum to be the same.

I know I am not understanding something here and I don't have much experience with bitwise operators.

mainnum = ((uint64_t)(nums[0] >> 16 | nums[1]) >> 48) | (nums[3] << 16 | nums[2]);

To my understanding, I am starting at nums[0] and I'd assume this should populate the first 16 bits with this value. From here, I am shifting to the right by 16-bits to put us at the 48 bits offset and replace the 16 bits after that with nums[1] (in which, I believe is the most significant on little endian).

From here, I then shift mainnum to the right by 48 bits to put us at the 16 bit offset. I then replace the next 16 bits with nums[3] and shift to the left by 16 bits and replace the rest with nums[2].

I definitely need more practice with bitwise operators (it is on my to-do list) and I'm sure I'm doing something silly here. I just wanted to see if anybody could correct me on what I'm doing wrong.

Any help is highly appreciated and thank you for your time!

  • Do you have to use bitwise operators to do that? This task can be easily done with `union` – Roy Avidan Dec 17 '20 at 19:49
  • 1
    Also, I believe that most of your problems come from the fact that you need to cast the `nums[i]` numbers to `uint64_t` before any arithmetic operation on them. – Roy Avidan Dec 17 '20 at 19:53
  • 1
    Building `mainnum ` isn't right. You have wrong indexes and wrong direction shifts. Cast each element to 64-bit before shifting too. – Weather Vane Dec 17 '20 at 19:58
  • 1
    A left shift, with `<<`, moves bits from low positions (1, 2, 4, 8,…) to higher positions. A right shift, with `>>`, moves bits from high positions to lower positions. A right shift of a 16-bit integer by 16 bits completely discards all the bits, producing zero. To assemble four 16-bit integers into a 64-bit integer, you need to shift three of them left by 16, 32, and 48 bits. – Eric Postpischil Dec 17 '20 at 19:59
  • 1
    When shifting, cast the operand being shifted to the desired result type. For example, `nums[3] << 48` will not work because the 16-bit `nums[3]` will only be promoted, automatically, to 32-bit `int` (in a typical C implementation), and the shift will be performed in that 32-bit type. So it cannot be shifted 48 bits, and that shift is not defined by the C standard. Instead, use `(uint64_t) nums[3] << 48`. – Eric Postpischil Dec 17 '20 at 20:00
  • 1
    Using shift operations is normally done when you want your code to be endian independent . If you instead want different behaviour for different endianness then `memcpy` or `union` is a far simpler option – M.M Dec 17 '20 at 20:01
  • The shift amounts you have are perverse, and the comments like `// ---- xxxx ---- ----` do not match the amounts. You are either going to shift word 0 by 0*16 bits, word 1 by 1*16 bits, word 2 by 2*16 bits, and word 3 by 3*16 bits or word 0 by 3*16 bits, word 1 by 2*16 bits, word 2 by 1*16 bits, and word 3 by 0*16 bits, depending on endianness. (There are implementations with mixed byte orders, but I doubt you are working with one.) – Eric Postpischil Dec 17 '20 at 20:02
  • Thank you for the comments everybody and I apologize for the silly question to begin with! I'm going to keep all of this in mind for when I start digging more deeper into bitwise operations, etc. – Christian Deacon Dec 18 '20 at 02:45

1 Answers1

2

You need the same/corresponding indexes and shift amounts that you used to create nums from num:

nums[0] = num >> 0;                 // xxxx ---- ---- ----
nums[1] = num >> 16;                // ---- ---- ---- xxxx
nums[2] = num >> 32;                // ---- ---- xxxx ----
nums[3] = num >> 48;                // ---- xxxx ---- ----

And, each term needs a (uint64_t) cast to force promotion to 64 bits. Otherwise, the shift will exceed the size used for intermediate terms on the right side of the assignment (e.g. they'll be done with int and/or unsigned int).

mainnum |= (uint64_t) nums[0] << 0;
mainnum |= (uint64_t) nums[1] << 16;
mainnum |= (uint64_t) nums[2] << 32;
mainnum |= (uint64_t) nums[3] << 48;

I prefer the above because it's cleaner/clearer and [when optimized] will produce the same code as doing it with a single statement:

mainnum =
    (uint64_t) nums[0] << 0 |
    (uint64_t) nums[1] << 16 |
    (uint64_t) nums[2] << 32 |
    (uint64_t) nums[3] << 48;
Craig Estey
  • 30,627
  • 4
  • 24
  • 48
  • Thank you! This works great and I'll keep this in mind for the future as well once I start digging deeply into bitwise operations, etc. – Christian Deacon Dec 18 '20 at 02:43