Just like the other “Dangerously Confusing Interfaces” posts, this one was also inspired by a real-world blunder that I made.
Here’s the background: usually, routines that accept data via a pointer from the caller either execute synchronously or copy the data into their own internal data structures for later processing. Take the venerable ‘fwrite’ from the C standard library as an example:
1 2 3 |
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); |
‘fwrite’ blocks until the data has been written, either to disk or to an internal buffer. In either case, once ‘fwrite’ returns, it doesn’t care about the original data anymore. That’s why it’s safe (and common practice) to pass a pointer to a local buffer on the stack:
1 2 3 4 5 6 |
void LogHello() { const char message[] = "Hello"; fwrite(message, sizeof(message), 1, g_log_file); } |
All standard library and POSIX APIs behave like ‘fwrite’, which is both, safe and convenient. However, with embedded systems, the story is different: in some cases, memory is so tight that additional buffers/internal storage can’t be afforded. Such functions don’t copy the provided data but only store a pointer to your data and expect the memory pointed-to by this pointer to be still valid long after the function call has returned. Here is an example from the AUTOSAR standard, which is used by almost all embedded automotive products:
1 2 3 |
Std_ReturnType NvM_WriteBlock(NvM_blockIdType blockId, const uint8* data); |
‘NvM_WriteBlock’ is used to store data to a given non-volatile memory block. However, what this function does is only enqueue a request for the given block ID together with the data pointer (not a copy of your data). This is done for the sake of efficiency, because there can be multiple write requests in parallel. The queue is later processed in another task, long after any local buffer would have been removed from the stack.
Passing a pointer to a buffer with automatic storage is an easy mistake to make, especially since such “non-copy” interfaces are so rarely encountered. How can “write-like” interfaces that don’t make a copy of the provided data be made safer, such that misuse is less likely? Obviously, just adding documentation is not enough — nobody reads documentation, especially in the heat of the moment.
In my view, the root of the problem is that such functions accept just about any pointer. What if the caller was forced to explicitly cast the pointer to another type? A type with a cunningly chosen typename, one that reminded the caller of the potential pitfall? Here is my approach:
1 2 3 4 5 6 7 |
typedef struct { char dummy; } uncopied_memory; void SomeWritelikeFunction(const uncopied_memory* data, size_t len); |
Whenever a pointer is passed to this function, developers have to write something like this to make the compiler happy:
1 2 3 |
SomeWritelikeFunction((uncopied_memory*) data, sizeof(data)); |
Typing ‘uncopied_memory’ should shake up even the most focused developers and remind them to double-check what they are passing into this function.
Of course, within ‘SomeWritelikeFunction’, the provided pointer needs to be cast back into something more useful, like a ‘const uint8_t*’. Further, note that the ‘dummy’ member within ‘uncopied_memory’ must not be used; it only exists to make sure that the cast to ‘uncopied_memory*’ in the calling function is safe: a pointer to a struct is aligned such that it is aligned with the struct’s first member, which is ‘char’ and ‘char’ has the weakest alignment requirements.