— Morihei Ueshiba
Sometimes, someone walks up to you and claims that there is a bug in your well-crafted code. Then, after having successfully proved that individual wrong, it occurs to you that there is indeed a bug—albeit a different one! Those are quite humbling experiences, but experiences that we should be most grateful for.
SETTING THE STAGE
This episode was triggered by feedback that I received from a reader regarding a “Dangerously Confusing Interfaces” post. In said post, I advise that instead of accepting a pointer to “uncopied” memory like this:
1 2 3 |
void WriteAsync(const void* data, size_t len); |
‘WriteAsync’ should rather take a pointer to an opaque data structure named ‘uncopied_memory’:
1 2 3 4 5 6 7 |
typedef struct { void* dummy; } uncopied_memory; void WriteAsync(const uncopied_memory* data, size_t len); |
“uncopied” memory means that for the sake of efficiency, the called function doesn’t copy the provided data but instead expects you to keep it alive and unchanged while the called function is executed asynchronously. Since the suggested interface change requires an explicit cast to an ‘uncopied_memory’ pointer, it’s a lot less likely that a temporary buffer allocated from the stack is passed accidentally. The idea of the proposed approach is that every call to ‘WriteAsync’ requires an explicit cast that acts as a reminder to the programmer that the buffer’s contents must be preserved.
For instance, if you wanted to pass a structure that I used in the previous installment of this series to ‘WriteAsync’, you would do it like this:
1 2 3 4 5 6 7 8 9 10 11 |
typedef struct { uint8_t level; uint16_t temperature; uint32_t force; } measurements_t; extern struct measurements_t my_measurements; ... WriteAsync((uncopied_memory*) &my_measurements, sizeof(my_measurements)); |
But back to the question. What the reader was worried about is that since ‘measurements_t’ and ‘uncopied_memory’ are by no means compatible, wouldn’t a cast to an ‘uncopied_memory’ pointer constitute a violation of the “strict aliasing rule“?
Actually, when it comes to the “strict aliasing rule,” the fact that these structs have incompatible members doesn’t really matter—even if you accessed the stored value through a pointer to a struct with an identical set of members you would be in trouble; if the tag names of the structs are different, it already counts as a violation of the “strict aliasing rule.”
The key word here is access. If you just create a pointer to incompatible types, everything is fine. Within ‘WriteAsync’ you just cast the received ‘uncopied_memory’ pointer into a ‘uint8_t’ pointer and access the provided data byte-wise, which is always safe, as you know (if you didn’t know, go back and read my previous post).
So far, so good. We don’t access stored memory through incompatible pointers; we only do pointer conversion, which is always safe, isn’t it? I replied to my reader that everything was fine, there was no violation of the “strict aliasing rule.”
Nevertheless, I couldn’t rid myself of this nagging feeling about whether the conversion/cast is really always safe.
POINTER CONVERSION RULES
The venerable book “The C Programming Language” by Brian Kernighan and Dennies Ritchie has this to say on pointer conversions:
A pointer to one type may be converted to a pointer to another type. The resulting pointer may cause addressing exceptions if the subject pointer does not refer to an object suitably aligned in storage. It is guaranteed that a pointer to an object may be converted to a pointer to an object whose type requires less or equally strict storage alignment and back again without change; the notion of
alignment” is implementation-dependent, but objects of the char types have least strict alignment requirements. As described in Par.A.6.8, a pointer may also be converted to type void * and back again without change.
Let me paraphrase: pointer conversion is safe provided the alignment requirements of the target type are less or equal to the alignment requirements of the source type. The converted pointer can be converted back to the original pointer without problems.
Though, the statement “The resulting pointer may cause addressing exceptions” is not clear to me. What does it mean? If the target type has stricter alignment requirements, do you get “addressing exceptions” when you create the pointer or when you access memory through it? Let’s assume that we are on a typical platform where objects of type ‘double’ are aligned on an 8-byte boundary and ‘chars’ have no alignment requirements (‘chars’ are aligned on a 1-byte boundary, so to speak.):
1 2 3 4 5 6 7 8 9 |
double PI = 3.1415927; char* pc = (char*) Π // (1) char byte0 = *pc; // (2) double* pd = (double*) &byte0; // (3) double d = *pd; // (4) |
The conversion (1) is 100% safe and so is the corresponding read-access (2): the alignment requirements of type ‘char’ are less than the alignment requirements of type ‘double’. (4) is 100% unsafe, but what about (3)? Aren’t we just creating a pointer? To find out, I had to dig deep into my copy of the C99 language standard. Eventually, I found what I call the “pointer conversion rule”:
6.3.2.3/7 A pointer to an object or incomplete type may be converted to a pointer to a different object or incomplete type. If the resulting pointer is not correctly aligned for the pointed-to type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer. When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object.
There you have it and much more precise than the paragraph from “The C Programming Language.” Believe it or not—statement (3), the sheer pointer conversion already gets you into the realm of undefined behavior. Who knew?
So what does this mean regarding the conversion/cast from a ‘measurements_t’ pointer to an ‘uncopied_memory’ pointer? As we know from the standard, it would be safe if the alignment requirements for ‘uncopied_memory’ were less or equal to the alignment requirements of ‘measurements_t’.
In the previous example, we had to deal with primitive types (‘char’, ‘double’) whose alignment requirements can easily be determined. In order to find out about the alignment requirements for structs, we need to dive once more into the C99 standard document:
6.7.2.1/13 A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
Meditate on this for a while. Paraphrased, this all means that the alignment requirements of a struct are the same as the alignment requirements of a struct’s first member. So the question boils down to this: Are the alignment requirements of a ‘void’ pointer (‘uncopied_memory’s first member) less or equal to the alignment requirements of a ‘char’ (‘measurements_t’s first member)?
Of course, they’re not! A pointer type (like void*) is more or less just an integer type in disguise that is capable of holding all the addresses of your system and as such, pointer types have the same alignment requirements as regular integer types. On a 32-bit platform, pointers typically comprise 4 bytes. Thus, on typical 32-bit platforms, they will need to be aligned on 4-byte boundaries.
By contrast, a character (like the first element of measurements_t) comprises exactly one byte and thus has no alignment requirement—it can be stored at any address in memory.
Since the alignment requirements of the first element of ‘uncopied_memory’ are stronger than the alignment requirements of ‘measurements_t’, we can conclude that my advice to cast to ‘uncopied_memory’ may yield undefined behavior. Not because of the “strict aliasing rule,” but because of a violation of the “pointer conversion rules.”
To solve the problem, the type of the ‘dummy’ member of ‘uncopied_memory’ needs to be changed to ‘char’, a type that has the weakest alignment requirements. I have updated the “Dangerously Confusing Interfaces” post accordingly.