Evolving a function: Part 1
A poorly communicated part of programming is the evolution of understanding the problem, coupled with the evolution of how to communicate the solution using the programming language. This is further exacerbated by the fact that evolution is not linear is vexing to both programmers and business people.
Take a common task in binary file parsing of reading a specific number of bytes from a file.
The goal of this and later posts is to walk-through the evolution of a function in C to achieve this task using well established unix API.
A starting point
#include <unistd.h> /* Assumed in later examples. */
int readn(int fh, char *buf, int size)
{
return read(fh, buf, size) == size;
}
This is a working solution, that returns 1 if size bytes were read, 0 otherwise.
Now let’s look at evolving this implementation. Let’s start with parameter validation.
Add parameter validation
#include <assert.h> /* Assumed in later examples. */
int readn(int fh, char *buf, int size)
{
assert(fh >= 0 && buf != NULL && size > 0); /* Pre-conditions. */
return read(fh, buf, size) == size;
}
This checks reasonable pre-conditions, but will cause the application to crash at runtime and the assert is typically disabled in debug builds.
Add runtime parameter validation
#include <errno.h> /* Assumed in later examples. */
int readn(int fh, char *buf, int size)
{
/* Pre-conditions. */
if (fh < 0 || buf == NULL || size <= 0) {
errno = EINVAL;
return -1;
}
return read(fh, buf, size) == size;
}
This now checks the conditions at runtime in all types of builds, sets errno and returns the code
-1 which consistent with the standard library read() functions.
Now consider if the pre-condition checking is both useful and correct.
All the pre-conditions are checked in single expression, and there is no way for the caller to
identify which argument is invalid. Looking at the manual page for read we can see that
this is a constriction of the error reporting interface available through read().
- For an invalid file handle
read()will return -1 and set errno to EBADF. - If an invalid pointer
read()will return -1 and set errno to EFAULT, except potentially in the case where size is 0. - Lastly
read()use expects the size to be described by the unsigned type size_t.
For points 1 and 2, the error checking is better left to the libc code, as not only is it written by experts and rigorously tested, it implements more sophisticated error checking, for example it will return EBADF if the file handle is not open for reading.
In the next part I will look at handling the size parameter, which is more complicated than it may first appear.