Is it legal write to a byte array in a union and read from an int to convert values in MISRA C?
Asked Answered
M

4

3

I guess this must have been asked before, but I could not get a specific yes/no answer.

I have this code snippet :

union integer_to_byte
{
    signed int  IntPart;
    unsigned char BytePart[2];
};

typedef union integer_to_byte I2B;

main()
{
   I2B u16VarNo;

   while(1)
   {
       // some code....
       u16VarNo.BytePart[1]= P1;

       // some more code ....
       u16VarNo.BytePart[0]= P2;

       // still more code ...
       if(u16VarNo.IntPart != 0xFFFF)
       {
       }
   }
}

Is this a legal way to use Unions in C ? From what I read; only the last assigned Union part is valid.So "u16VarNo.BytePart[1]" is not determinate ? The code which I have written works perfectly as expected,but I thought I would get it clarified.

TIA.

Munafo answered 24/3, 2020 at 14:31 Comment(7)
This is valid only for native ints of 2 bytes (16 bits). Maybe IntPart should be of type int16Lolland
AFAIK the legal way to use unions is so that different types may be stored in the same memory: not to use for type conversion. You'll fall over on endianness. I suggest converting with mask and shift.Tench
misra forbids any pointer punning I think. Only arithmeticsCrump
if(u16VarNo.IntPart != 0xFFFF) looks strange when you have signed int IntPart;. I would expect if(u16VarNo.IntPart != -1). But as you have named it u16VarNo you seem undecided whether you are using signed or unsigned values.Tench
Or you could define the byte array as unsigned char BytePart[sizeof int];Sam
MISRA C:2012, 19.2 - The union keyword should not be used. well?Delight
@ Weather Vane : I am using it as unsigned , i typed out the declaration wrongly.Munafo
D
5

Is it legal write to a byte array in a union and read from an int to convert values in MISRA C?

No. Unions shall not be used.

MISRA C:2004, 18.4 - Unions shall not be used.
MISRA C:2012, 19.2 - The union keyword should not be used

The rule in MISRA C:2004 follows with:

It is recognised nonetheless that there are situations in which the careful use of unions is desirable in constructing an efficient implementation. In such situations, deviations to this rule are considered acceptable provided that all relevant implementation-defined behaviour is documented. This might be achieved in practice by referencing the implementation section of the compiler manuals from the design documentation.

and

The use of deviations is acceptable for (a) packing and unpacking of data, for example when sending and receiving messages, and (b) implementing variant records provided that the variants are differentiated by a common field.

Your use does not fit those cases.

Delight answered 24/3, 2020 at 21:59 Comment(1)
Thanks Kamil. Isn't this a case of packing 2 bytes into an integer ? Or could you explain further when convenient.Munafo
L
5

Formally, unions are not allowed, though this rule has been relaxed to Advisory from MISRA-C:2004 to MISRA-C:2012. The main purpose of banning union was always to prevent really dumb things like creating "variant types" à la Visual Basic, or by re-using the same memory area for unrelated purposes.

But to use union for the purpose of type punning is common practice, particularly in embedded systems, so banning them is cumbersome too. Rule 19.2 raises the valid concern that writing to one union member and then reading from another invokes unspecified or implementation-defined behavior. Unspecified if the members don't match, otherwise implementation-defined since there are conversions.

Further concerns from MISRA regarding the breaking the rule are padding, alignment, endianess and bit order (in case of bit-fields). These are also valid concerns - in your specific example, many of these are potential issues.

My advise is this:

  • Deviate from this rule only if you know what you are doing. Valid cases of type punning through unions are register map declarations and serialization/de-serialization code.
  • Using a union for the purpose of getting high and low bytes of some int is not a valid use case, it's just bad... because it makes the code needlessly non-portable for nothing gained. Assuming 16 bit system, there's absolutely no reason why you can't replace this union with portable bit operators instead:

    int16_t some_int = ...;
    uint8_t ms = (uint16_t)some_int >> 8;
    uint8_t ls = some_int & 0xFF;
    
  • Ensure that padding isn't an issue by (pseudo code) _Static_assert( sizeof(the_union) == sizeof(all_members)...

  • Document any code that disables padding, both in your source code with comments and in your MISRA-C implementation document. Stuff like #pragma pack(1) or whatever your specific compiler uses.
Lollapalooza answered 25/3, 2020 at 10:52 Comment(2)
"The main purpose of banning union was always to prevent really dumb things like creating 'variant types'" - can you explain this a bit? I thought that using a union for variant types/reusing memory is what it was designed for, opposed to type punning which is a (useful) hack.Bluma
@Groo Yes it was possibly designed for that, but such "variant" types are inefficient and generally bad. There was originally two sister rules in MISRA, one saying that an area of memory shall not be used for unrelated purposes, and one saying that unions shall not be used. From the former, they give examples like this: "a program might try to access data of one type from the location when actually it is storing a value of the other type (e.g. due to an interrupt)." Whereas type punning is accessing an area of memory for related purposes, such as serializing a larger data type.Lollapalooza
C
3

union punning especially using the gcc family (or IAR, GHS, ARM and many other compilers) is 100 % fine.

All compilers I know follow the footnote 95.

If the member used to access the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called "type punning"). This might be a trap representation.

Crump answered 24/3, 2020 at 20:29 Comment(1)
Is this a case of legitimate union punning ? Would the "u16VarNo.BytePart[1]= P1;" be retained when reading u16VarNo.IntPart ?Munafo
K
1

In ordinary C — only following the rules of ISO C, not the additional rules added by MISRA — the construct shown is conforming, but not strictly conforming, because it depends on unspecified behavior. "Unspecified" means, in this case, that the read from u16VarNo.IntPart is allowed to give you a value that doesn't make any sense at all, but it is not allowed to crash your program, and the compiler is not allowed to optimize on the assumption that the read can never be executed.

The precise rule is C2011 section 6.2.6.1 paragraph 7:

When a value is stored in a member of an object of union type, the bytes of the object representation that do not correspond to that member but do correspond to other members take unspecified values.

u16VarNo.BytePart[1]= P1 stores a value in a member of an object of union type. That union has two other members, BytePart[0] and IntPart¹; both of them cover at least one byte of the object representation that don't correspond to BytePart[1] (depending on exactly how big a signed int is); that byte takes on an unspecified value when you write to BytePart[1].

The practical upshot of this is that after

u16VarNo.BytePart[1] = 0xFF;
u16VarNo.BytePart[0] = 0xFF;

you are allowed to read from uint16VarNo.IntPart but the value you get may well be garbage. In particular

assert(u16VarNo.IntPart == 0xFFFF);   // THIS ASSERTION MAY FAIL

I am only vaguely familiar with MISRA's additional rules but I have the impression that they flat-out forbid you to do anything even sort of like this.


The correct way to convert two bytes of data from an external source into a 16-bit signed integer is with helper functions like this:

#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[static 2])
{
    uint32_t val = (((uint32_t)data[0]) << 8) | 
                   (((uint32_t)data[1]) << 0);
    return ((int32_t) val) - ((int32_t)0x10000);
}

int16_t le16_to_cpu_signed(const uint8_t data[static 2])
{
    uint32_t val = (((uint32_t)data[0]) << 0) | 
                   (((uint32_t)data[1]) << 8);
    return ((int32_t) val) - ((int32_t)0x10000);
}

There are two functions because you need to know, and specify in your code, which endianness the external source provides the data in. (This is another, unconnected reason why your original code can't be relied on.) You have to use a 32-bit unsigned intermediate because the constant 0x10000 doesn't fit in a 16-bit register. You have to include all of those explicit casts to stdint.h fixed-width types because otherwise the "usual arithmetic conversions" have a good chance of picking the wrong signedness for each step. (The shifts and ors must be done in unsigned arithmetic, and the final subtraction in signed arithmetic.)


¹ Whether or not BytePart[0] and BytePart[1] are two separate members of the union is ill-specified; this is an instance of the "what exactly is an 'object'" argument that has been unresolved since the original publication of the 1989 C standard, despite multiple attempts to fix the wording. However, it's not safe to assume compilers won't treat them as two separate objects.

Kellner answered 24/3, 2020 at 15:9 Comment(21)
It stops to be so strict when you read. Footnote 95 says If the member used to access the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called "type punning"). This might be a trap representation. The union punning has big machine (UNISYS, IBM) oponents in the committee.Crump
@P__J__ Footnotes are not normative, but that's actually not the important thing right now -- the important reason why OP's code won't work is that the write to BytePart[0] is allowed to clobber the storage for BytePart[1] and vice versa. And it is reasonably likely to do just that if (for instance) the compiler decides to emit a word-sized store.Kellner
@zwol: the write to BytePart[0] is allowed to clobber the storage for BytePart[1] and vice versa. That's completely insane. What would be the rationale for allowing such side-effects and not disallowing arrays and structures as union members?Sam
6.2.6.1p7: When a value is stored in a member of an object of union type, the bytes of the object representation that do not correspond to that member but do correspond to other members take unspecified values. When writing to BytePart[0], the union object member is BytePart with type unsigned char[2], hence BytePart[1] is part of the same member, thus is not affected.Sam
@chqrlieforyellowblockquotes (1) It's so the compiler can use word-sized writes if it wants; this is necessary on hardware that doesn't have byte writes, which is unusual nowadays but not as unusual as some of the things the standard provides for. (2) Please see the footnote on my answer. (3) I will not debate whether compilers should preserve BytePart[1] when BytePart[0] is written. It is a fact that some of them don't and therefore OP's code is unreliable. If you see a factual error in what I wrote, tell me, but otherwise take it up with the C committee.Kellner
My reading of the 6.2.6.1p7 above is compilers must preserve BytePart[1] when BytePart[0] is written because they are part of the same member of the union. The compiler may indeed have to generate word writes on some legacy architectures, but it must preserve the parts of the word that belong to other entries of the same array.Sam
it is not allowed to crash your program I don't see where "unspecified behavior" precludes trap representations that could crash a program though.Precambrian
@AndrewHenle The difference between an "unspecified value" and an "indeterminate value" is precisely that an unspecified value is not allowed to be a trap representation. See C11 § 3.19.2 and 3.19.3.Kellner
@chqrlieforyellowblockquotes Whether or not your interpretation is correct is disputed among C committee members and implementors. This is what I meant when I said that "this is an instance of the 'what exactly is an object' argument."Kellner
@Kellner But still, 6.2.6.1p6 and 6.2.6.1p7 only refer to the bytes of a union that do not correspond to the parts of the union being written to. Footnote 95 states "If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ''type punning''). This might be a trap representation."Precambrian
This discussion is illuminating.Does this mean its a compiler dependant? The code in question is running without problems on quite a few devices,for more than a few years.I am not concerned about the portablility,more about what zwol said : the important reason why OP's code won't work is that the write to BytePart[0] is allowed to clobber the storage for BytePart[1] and vice versa. And it is reasonably likely to do just that if (for instance) the compiler decides to emit a word-sized store.Munafo
I meant I am not concerned about portability is because it is a legacy device and only needs support and not development.Munafo
@Munafo "It's compiler and target architecture dependent" is a fair one-sentence practical takeaway. For code in legacy support mode, the main thing I would be worrying about is that a new version of whichever compiler you're using could make it stop working. For new code, though, use the bit-shift conversion functions I suggested.Kellner
@Kellner : Thats a good caveat.We usually backup the compiler and the code together.Munafo
@Kellner : as an aside : return ((int32_t) val) - 0x10000u; Is this to take care of signedness ? Any short explanation possible which will not divert the thread ? TIA.Munafo
@Munafo Yes, that's the bulletproof way to convert a 16-bit unsigned quantity (in a 32-bit variable) to a 16-bit twos-complement signed quantity. There are several things that can go wrong if you just cast it; for instance you might wind up with 0x8000 turning into zero.Kellner
@zwol: your contraption to avoid implementation defined conversion would be less confusing with an explicit cast: return (int16_t)(((int32_t)val) - 0x10000u);. Actually, could you explain how it works? I don't see how it avoids the implementation defined behavior, and if you can assume 2's complement representation, how would casting fail?Sam
@chqrlieforyellowblockquotes Please post a new question about that.Kellner
@zwol: be my guest: #60864950 :)Sam
BytePart[0] and BytePart[1] are not members of the union. The members are IntPart and BytePart .Wallow
@zwol: would you care to provide your explanation by answering my question?Sam

© 2022 - 2024 — McMap. All rights reserved.