Efficient tests with optional diagnostic in C
Category: Sysadmin.
I love C because it is a low level high level programming language.
I hate C for exactly the same reason.
If you have already wrote a C program that rely on a lot of system calls and tests to check the execution environment before doing something, you have probably entered the hell of verifying loads of return values and displaying diagnostic messages for each failure case.
Basically, the code look like this for system call:
if ((ret = systemcall(parameters))) { ... } else { perror("systemcall:"); }
... and like this for regular tests:
if (v1 > v2) { ... } else { warn("%d <= %d: that should not happen", v1, v2); }
Multiple tests are nested at the ... location, thus, if you have 6 test, you have at least 25 lines of code and 7 levels of indentation :-/.
Building sets of macros
The idea is that there are two things to take into account:
- the test itself;
- the diagnostic message for the user if an error occur.
Under some circumstances, the user does not need the diagnostic message. Only the program has to take the return value into account for the rest of the execution (a missing directories may be created for example).
The easiest way to do this is to declare three macros:
- the test macro;
- the diagnostic macro;
- a macro that calls the two other in a short form.
Because of this dependency, and to fit into nearly any structure, each macro needs to be a boolean expression, the test macro returning non-zero on success, zero on failure, and the diagnostic macro returning zero all the time. Moreover, all the macros may accept the same arguments.
We can then define a set of macro like this:
#define STAT(file) (stat(file, &file##_stat) == 0) #define STAT_ERR(file) (warn("stat: %s", file), 0) #define STAT_E(file) (STAT(file) || STAT_ERR(file))
... and use it like this:
int main(int argc, char *argv[]) { const char *source, *target; struct stat source_stat, target_stat; ... source = argv[1]; target = argv[2]; if (STAT_E(source) && STAT_E(target)) { ... } else { exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); }
Too easy! Of course, we can also use out STAT
macro in the code each time we want to try to stat(2) a file without reporting diagnostic to the user on failure.
A real-world example
Remember the 25 lines of code for diagnostic messages for my six-tests example? It comes form a tool that accept two directories as arguments: both need to exist, to be on the same device and to be two distinct directories. Let's build a few macro sets:
#define STAT(file) (stat(file, &file##_stat) == 0) #define STAT_ERR(file) (warn("stat: %s", file), 0) #define STAT_E(file) (STAT(file) || STAT_ERR(file)) #define SAMEDEVICE(source, target) \ (source##_stat.st_dev == target##_stat.st_dev) #define SAMEDEVICE_ERR(source, target) \ (warnx("%s and %s are not on the same device.", source, target), 0) #define SAMEDEVICE_E(source, target) \ (SAMEDEVICE(source, target) || SAMEDEVICE_ERR(source, target)) #define NOSAMEFILE(source, target) \ (source##_stat.st_ino != target##_stat.st_ino) #define NOSAMEFILE_ERR(source, target) \ (warnx("%s and %s are the same file.", source, target), 0) #define NOSAMEFILE_E(source, target) \ (NOSAMEFILE(source, target) || NOSAMEFILE_ERR(source, target)) #define ISDIR(file) (file##_stat.st_mode & S_IFDIR) #define ISDIR_ERR(file) (warnx("%s is not a directory.", file), 0) #define ISDIR_E(file) (ISDIR(file) || ISDIR_ERR(file))... and the code magically become more readable:
int main(int argc, char *argv[]) { ... if (STAT_E(source) && STAT_E(target) && SAMEDEVICE_E(source, target) && NOSAMEFILE_E(source, target) && ISDIR_E(source) && ISDIR_E(target)) { err = link_dir(source, strlen(source), target, strlen(target)); } else { err = EXIT_FAILURE; } exit(err); }
Because of the left-to-right precedence, the result is strictly equal to the nested tests, but the details of the implementation are not visible, making it easier to focus on the algorithm and makes the code easier to maintain.