I've been trying to figure out how to access a mapped buffer from C++17 without invoking undefined behavior. For this example, I'll use a buffer returned by Vulkan's vkMapMemory
.
So, according to N4659 (the final C++17 working draft), section [intro.object] (emphasis added):
The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition (6.1), by a new-expression (8.3.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2).
These are, apparently, the only valid ways to create a C++ object. So let's say we get a void*
pointer to a mapped region of host-visible (and coherent) device memory (assuming, of course, that all the required arguments have valid values and the call succeeds, and the returned block of memory is of sufficient size and properly aligned):
void* ptr{}; vkMapMemory(device, memory, offset, size, flags, &ptr); assert(ptr != nullptr);
Now, I wish to access this memory as a float
array. The obvious thing to do would be to static_cast
the pointer and go on my merry way as follows:
volatile float* float_array = static_cast<volatile float*>(ptr);
(The volatile
is included since this is mapped as coherent memory, and thus may be written by the GPU at any point). However, a float
array doesn't technically exist in that memory location, at least not in the sense of the quoted excerpt, and thus accessing the memory through such a pointer would be undefined behavior. Therefore, according to my understanding, I'm left with two options:
memcpy
the dataIt should always be possible to use a local buffer, cast it to std::byte*
and memcpy
the representation over to the mapped region. The GPU will interpret it as instructed in the shaders (in this case, as an array of 32-bit float
) and thus problem solved. However, this requires extra memory and extra copies, so I would prefer to avoid this.
new
the arrayIt appears that section [new.delete.placement] doesn't impose any restrictions on how the placement address is obtained (it need not be a safely-derived pointer regardless of the implementation's pointer safety). It should, therefore, be possible to create a valid float array via placement-new
as follows:
volatile float* float_array = new (ptr) volatile float[sizeInFloats];
The pointer float_array
should now be safe to access (within the bounds of the array, or one-past).
So, my questions are the following:
static_cast
indeed undefined behavior? new
usage well-defined? As a side note, I've never had an issue by simply casting the returned pointer, I'm just trying to figure out what the proper way to do this would be, according to the letter of the standard.
Non-memory mapped region includes internal general purpose and special function registers of CPU. These registers do not have addresses. We can access them using register names in assembly language. In C programming, we can access these registers using inline assembly language features of c programming.
Advantages of Undefined Behavior. C and C++ have undefined behaviors because it allows compilers to avoid lots of checks. Suppose a set of code with greater performing array need not keep a look at the bounds, which avoid the needs for complex optimization pass to check such conditions outside loops.
Memory Mapping in Embedded Processors and Microcontrollers Microcontrollers or microprocessors have two types of memory regions such as memory mapped region and non-memory mapped region.
For instance, if we take the example of ARM Cortex M4 32-bit microcontroller, its addressable memory space is 2^32 which is equal to 4 gigabytes of memory. Each byte of this memory space has a unique memory address and the Cortex M4 microcontroller can access each memory location either to read and write data to each memory location.
The C++ spec has no concept of mapped memory, so everything to do with it is undefined behavior as far as the C++ spec is concerned. So you need to look to the particular implementation (compiler and operating system) that you are using to see what is defined and what you can do safely.
On most systems, mapping will return memory that came from somewhere else, and may (or may not) have been initialized in a way that is compatible with some specific type. In general, if the memory was originally written as float
values of the correct, supported form, then you can safely cast the pointer to a float *
and access it that way. But you do need to know how the memory being mapped was originally written.
As per the Standard, everything involving hardware-mapped memory is undefined behavior since that concept does not exist for the abstract machine. You should refer to your implementation manual.
Even though hardware-mapped memory is undefined behavior by the Standard, we can imagine any sane implementation providing some obeys common rules. Some constructs are then more undefined behavior than others (whatever that means).
Is the simple
static_cast
indeed undefined behavior?volatile float* float_array = static_cast<volatile float*>(ptr);
Yes, this is undefined behavior and have been discussed many times on StackOverflow.
Is this placement-new usage well-defined?
volatile float* float_array = new (ptr) volatile float[N];
No, even though this looks well defined, this is implementation dependent. As it happens, operator ::new[]
is allowed to reserve some overhead1, 2, and you cannot know how much unless you check your toolchain documentation. As a consequence, ::new (dst) T[N]
requires an unknown amount of memory greater or equal to N*sizeof T
and any dst
you allocate might be too small, involving buffer overflow.
How to proceed, then?
A solution would be to manually build a sequence of floats:
auto p = static_cast<volatile float*>(ptr); for (std::size_t n = 0 ; n < N; ++n) { ::new (p+n) volatile float; }
Or equivalently, relying on the Standard Library:
#include <memory> auto p = static_cast<volatile float*>(ptr); std::uninitialized_default_construct(p, p+N);
This constructs contiguously N
uninitialized volatile float
objects at the memory pointed to by ptr
. This means you must initialize those before reading them; reading an uninitialized object is undefined behavior.
Is this technique applicable to similar situations, such as accessing memory-mapped hardware?
No, again this is really implementation-defined. We can only assume your implementation took reasonable choices, but you should check what its documentation says.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With