Wander Lairson Costa

Highway to hell: C++ enums and bit fields

• cpp

C++ enums historically gave me some headaches due to the fact that compilers are free to choose the size of the type for whatever criteria they think. This makes particularly hard to write portable code among compilers from ABI point of view (Visual C++ vs C++ Builder, anyone?).

Some days ago I was debugging a problem with dali-toolkit and the enum story scalated to another level.

Let me give you some background: dali-toolkit loads textures using a background worker thread that is responsible to load the image files to memory. I was looking why some textures didn’t load in Windows. To make the asynchronous loading possible it implements a small state machine and it keeps all information about the texture in a type called TextureInfo.

Here is a partial definition of the structure:

struct TextureInfo {
		// ....
    LoadState loadState:4;         ///< The load state showing the load progress of the Texture
    FittingMode::Type fittingMode:3; ///< The requested FittingMode
    Dali::SamplingMode::Type samplingMode:3; ///< The requested SamplingMode
    StorageType storageType:2;     ///< CPU storage / GPU upload;
		// ....
};

All these custom types are enums defined elsewhere. Yes, you read right, enums + bit fields, what possibly can go wrong? To illustrate the root cause of the bug, let’s write a toy sample:

enum Animal {
    Cat,
    Dog,
    Horse,
    Turtle,
};

enum Breed {
    BorderCollie,
    GoldenRetriever,
    GermanShepard,
    Husky,
};

enum Adopted {
    No,
    Yes,
};

struct AnimalInfo {
    Animal animal:2;
    Breed breed:2;
    Adopted adopted:1;

    AnimalInfo(Animal a, Breed b, Adopted ad)
        : animal(a), breed(b), adopted(ad) {}
};

int main(void) {
    AnimalInfo animal(Animal::Dog, Breed::GermanShepard, Adopted::Yes);

    auto breed = animal.breed;

    return sizeof(AnimalInfo);
}

Now, using godbolt, we can view the assembly code generated by different compilers. In particular, let’s look at the assembly code for the main function. Firstly, the the gcc generated code. I added comments to the parts interesting to us:

main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        lea     rax, [rbp-8]
        mov     ecx, 1
        mov     edx, 2
        mov     esi, 1
        mov     rdi, rax
        call    AnimalInfo::AnimalInfo(Animal, Breed, Adopted)
        movzx   eax, BYTE PTR [rbp-8] // load the information
        shr     al, 2   	// breed is in the bits 2 and 3, so shift right 2 bits
        and     eax, 3  	// breed is only two bits long
        movzx   eax, al  	// eax = breed value
        mov     DWORD PTR [rbp-4], eax // store it in our variable
        mov     eax, 4    // AnimalInfo is 4 bytes long
        leave
        ret

Here is clang. It uses different registers but is essentially the same code:

main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     dword ptr [rbp - 4], 0
        lea     rdi, [rbp - 8]
        mov     eax, 1
        mov     esi, eax
        mov     edx, 2
        mov     ecx, eax
        call    AnimalInfo::AnimalInfo(Animal, Breed, Adopted)
        mov     r8b, byte ptr [rbp - 8]
        shr     r8b, 2
        and     r8b, 3
        movzx   eax, r8b
        mov     dword ptr [rbp - 12], eax
        mov     eax, 4
        add     rsp, 16
        pop     rbp
        ret

Now the msvc compiler:

main PROC
$LN3:
  sub rsp, 56 ; 00000038H
  mov r9d, 1
  mov r8d, 2
  mov edx, 1
  lea rcx, QWORD PTR animal$[rsp]
  call AnimalInfo::AnimalInfo(Animal,Breed,Adopted) ; AnimalInfo::AnimalInfo
  mov eax, DWORD PTR animal$[rsp]
  shl eax, 28
  sar eax, 30 // OH NO! SIGNED SHIFT!!!
  mov DWORD PTR breed$[rsp], eax
  mov eax, 4
  add rsp, 56 ; 00000038H
  ret 0
main ENDP

Before commenting about the bug generated by msvc, let me praise the compiler optimizer, it got the breed value in two instructions, while clang and gcc needed three. Very smart! Too bad that code is wrong. I think it is better to undestand the issue if we view the bit layout of the word holded by AnimalInfo:

	-------------------------------------
	|  31-5  |    4    |  3-2  |  1-0   |
	-------------------------------------
	| Unused | Adopted | Breed | Animal |
	-------------------------------------

So, the code shifts 28 bits to left, moving Breed to the bits 31 and 30, then it shifts 30 bits to the right, but it does a signed shift, filling the shifted bits with the value of the most significant bit. The Breed value is GermanShepard, which is 2. So, when we shift left, the bit 31 will hold the value 1, and when we shift right, we end up with the final value 0xfffffffe! The reason this happens is because msvc assumes the enum is a signed integer by default and, combined with the use of bit fields, yields the bug. This is precisely what happened in dali-toolkit.

Once we understood the problem, we can solve it easily by forcing the enum to an unsigned integer:

enum class Animal: unsigned int {
    Cat,
    Dog,
    Horse,
    Turtle,
};

enum class Breed: unsigned int {
    BorderCollie,
    GoldenRetriever,
    GermanShepard,
    Husky,
};

enum class Adopted: unsigned int {
    No,
    Yes,
};

struct AnimalInfo {
    Animal animal:2;
    Breed breed:2;
    Adopted adopted:1;

    AnimalInfo(Animal a, Breed b, Adopted ad)
        : animal(a), breed(b), adopted(ad) {}
};

int main(void) {
    AnimalInfo animal(Animal::Dog, Breed::GermanShepard, Adopted::Yes);

    auto breed = animal.breed;

    return sizeof(AnimalInfo);
}

Now the generated code does what it is supposed to do:

main PROC
$LN3:
  sub rsp, 56 ; 00000038H
  mov r9d, 1
  mov r8d, 2
  mov edx, 1
  lea rcx, QWORD PTR animal$[rsp]
  call AnimalInfo::AnimalInfo(Animal,Breed,Adopted) ; AnimalInfo::AnimalInfo
  mov eax, DWORD PTR animal$[rsp]
  shr eax, 2
  and eax, 3
  mov DWORD PTR breed$[rsp], eax
  mov eax, 4
  add rsp, 56 ; 00000038H
  ret 0
main ENDP
comments powered by Disqus