A preprocessor hack to implement “with” blocks in pure C

Alright, everyone, get out your pencils: it’s time for a quick test on the C language. Don’t worry if you didn’t study; the test will be rather painless, and you should be able to follow along even if you don’t really know C that well. Here it is:

Johnny is trying to design a function to be integrated into the codebase of an evil genius. The function must meet the following specifications:

  1. The function takes two parameters, an unsigned integer password and a string fname.
  2. The function tests whether password is equal to the Number of Evil (NOE), 69381U.
  3. If the test succeeds (if password is equal to NOE), then the function must write the secret evil plans to a file named fname.
  4. If the password doesn’t match, then we have an intruder, and the function should write a log message warning of the event to the standard output.

Here is his function.

void write_evil_plans(unsigned password, char *fname)
{
    if (password == 69381U) {
        FILE *f = fopen(fname, "w");
        fprintf(f, "You have successfully been authenticated. The evil plans are as follows: ...");
    } else { 
        printf("There has been an intruder. Please make a note of it.\n");
    }
    return;
}

What are all the problems with the function Johnny wrote?

…….

Alright, pencils down. There are two big problems with this function. You probably caught the first one: Johnny never closed his file! Now, whenever that function is called and the password is verified, a new file handle is created, and it’s never going to be closed. This is a classic example of a memory leak: we allocate a resource and lose the pointer to it so that freeing it becomes impossible. God help you if you call Johnny’s write_evil_plans more than a few times over the course of the runtime of the program. This problem is fixed easily enough, though; all we have to do is throw a fclose(f); right after the call to fprintf.

The second problem is more subtle: this program is unsafe in that it’s likely to cause a runtime crash. If fopen fails, then it will return a null pointer. Once it does, the call to fprintf will dereference the null pointer. BOOM! Instant undefined behavior. Johnny will have to restructure the function a bit in order to fix this problem: he’ll have to test whether f == NULL, execute the fprintf and the fclose only if that test fails, and take an appropriate action in case there was an I/O error.

These two issues — memory leaks and unchecked null pointers — are among the most pervasive problems in pure C code, and they happen for basically the same reason: writing robust, memory-safe C code is hard, and programmers are likely to fail to get it right. Anytime you call malloc, or you call a function that calls malloc you have to make sure to check for NULL. You then have to make sure you free that pointer somewhere. While these checks aren’t technically hard to implement, they’re easy to forget about if you fail to structure your code correctly. Moreover, C programs with memory errors are generally hard to debug, so these errors can be hard to spot until something catastrophic happens.

Other languages have different approaches to dealing with memory allocation robustly. Scripting languages eliminate memory leaks by using garbage collection. In Lua, for example, you can open and close files just as in C, but unclosed files are closed automatically once their file handles are garbage collected. C++ isn’t garbage-collected, but C++ code generally makes heavy use of the RAII model, which puts cleanup code in the destructors of objects, and therefore ensures that cleanup code will be executed deterministically when objects go out of scope.

In the general case, we can’t easily adapt either of those solutions to the C language. C has no facilities for garbage collection built-in, and there is no way to trigger automatic cleanup for objects. However, there is an idiom found in high-level programming languages that we can port over to C with a little preprocessor magic.

“With” to the rescue

In Python, you have the with statement, which allows the programmer to tie resource management to the scope of a variable:

with open(fname, 'w') as f:
    f.write('You have successfully been authenticated. The evil plans are as follows: ...')

After the write call, the file f is automatically closed. While we can’t capture the more powerful RAII idiom in C, we can simulate with statements like this one in C. Thanks to C’s powerful preprocessor and scoping support, we can capture all logic relating to resource-management and error-checking in a single macro. Here’s a with macro that works on files:

#define WITH_FILE(varname, fname, options, block, error) do {         \
    FILE *const varname = fopen(fname, options);                      \
    if (varname) { block; fclose(varname); } else { error; }          \
  } while (0)

Simple, eh? The 5 arguments are:

  1. The name of the variable we want to create.
  2. The name of the file (as a string) that we want to open.
  3. The options string we want to pass to fopen.
  4. The block that we want to execute with the file.
  5. The block that we want to execute if the file open operation fails.

Here’s how we would use that macro in Johnny’s program above:

void write_evil_plans(unsigned password, char *fname)
{
    if (password == 69381U) 
        WITH_FILE(f, fname, "w", {
            fprintf(f, "You have successfully been authenticated...");
          }, { 
            fprintf(stderr, "There was an I/O error.\n");
          });
    else 
        printf("There has been an intruder. Please make a note of it.\n");
    return;
}

In this case, if the fopen operation succeeds, the first block gets executed (that is, the evil plan is written to the user’s file), and if it doesn’t, the second block gets executed (that is, the error message is written to stderr). The macro gives us a few interesting advantages:

  • The macro eliminates a lot of boilerplate (the FILE* declaration, the NULL check, and the calls to fopen and fclose).
  • Because the macro takes 5 arguments, there will be a compile error if you try to omit the error-checking block. Of course, if you don’t want to take any special action in case of an I/O error, you can just throw in an empty block as the argument.
  • The macro behaves sensibly; the declared variable will only exist within the blocks you pass to the function-like macro, and will not be accessible elsewhere. Calls to WITH_FILE can be nested without a problem, even if the declared variables have clashing names.
  • Thanks to the structure of the macro being a do-while loop, you can write WITH_FILE(...); anywhere you would be able to put a normal statement. Of course, the conditional check of the loop is easy for compilers to optimize out.
  • Because the declared variable is const, the programmer can’t edit the contents of the variable. This means the call to fclose is completely safe.

This approach can be applied in other contexts as well, of course. We can, for example, write a with wrapper to malloc that allocates and frees an array of objects automatically:

#define WITH_ALLOC(varname, type, count, block, error) do {           \
    type *const varname = malloc(count * sizeof(type));               \
    if (varname) { block; free(varname); } else { error; }            \
  } while (0)

Here, the type parameter is the type of the object we want to allocate, and count is the number of consecutive objects we want to allocate. For example, if type is int, count is 300, and varname is a, then a will be declared as an int*, and it will be allocated to point to an array of 300 ints before running the block. This macro has the added advantage that it makes it impossible for us to give the wrong argument to malloc, which will prevent a particularly infuriating kind of memory error. Here’s an example usage:

  WITH_ALLOC(buffer, char, 256, { // buffer is a char*                                    
      WITH_FILE(file, "hello.txt", "r", { // file is a FILE*                            
          fgets(buffer, 256, file);
          printf("%s", buffer);
        }, {/* do nothing if I/O error */});
    }, {/* do nothing if malloc error */});

So, under the right circumstances (namely, you need to allocate a resource at the same time a variable goes into scope, and you want to free that resource as soon as the variable goes out of scope), using a macro like this might help you write robust and safe code. Of course, it’s questionable whether you should actually use this in a production environment — people disagree about whether it’s alright to use macros at all, and if you accidentally mix up the order of the arguments to these macros the result will probably be a confounding syntax error — but wrapping your resource allocations in this way can help make your pure C code a bit safer.