Making functions public for tests only… with C macros!

Note from the future (2024-09-05): in the first version of this post, I used the identifier _static instead of STATIC for a macro. I have recently found out that one should almost never use identifiers starting with underscore, so I have edited this post accordingly. See this StackOverflow question for an explanation.

As a programmer, I often face this dilemma: should I make this function private to improve encapsulation, or should I make it public so that I can write tests for it? I believe this problem is especially felt in scientific computing, or when implementing big, complex algorithms whose many small substeps have no place in a public interface, but should be unit-tested anyway.

Until recently, I essentially had two ways to deal with this (with a strong preference for the first one):

But a few days ago I thought of a cool trick (that realistically has been known for at least 45 years, I just was not aware of it before) to solve this problem for my C projects, using conditional compilation. Let’s dive in!

Function visibility in C

By default, functions in C are “public”, by which I mean visible to any other translation unit (file). For example, say you have the following files:

foo.c:

int foo(int x, int y) {
    return 42*x - 69*y;
}

main.c:

#include <stdio.h>

int foo(int, int); // Function prototype

int main() {
    int z = foo(10, 1);
    printf("%d\n", z);
    return 0;
}

You can build them with gcc foo.c main.c, and the program will run correctly and output 351. Usually, the function prototype is put in a separate foo.h file and it is included in main.c with #include "foo.h".

This works because a C program is built into an executable in two steps: compiling and linking. During the first of these two, each file is translated into object code; if the compiler finds a reference to a function whose body is not present in the same file - like our foo() in main.c - it does not complain, but it trusts the programmer that this function is implemented somewhere else. Then it is the turn of the linker, whose job is exactly is to put together the object files and resolve these function calls; the linker does complain if the body of foo() is nowhere to be found.

All of this is different for functions marked as static. These are only visible inside the file where they are defined.

Why make functions static?

There are a couple of reasons why one should make (some) functions static:

To illustrate the second point, I have put all the code of the previous example in the same file main2.c. You can compile it with gcc -O1 -S main2.c to enable optimizations and generate the assembly code instead of an exectuable. I have uploaded the output here: main2.s. Then you can do the same with main3.c, whose only difference is that foo() is now static, and check the resulting main3.s.

As you can see, the section labelled foo: has disappeared. This is because the compiler knows that it will not be needed anywhere else; it inlined it everywhere it saw a reference to it and called it a day.

You may also see that foo was actually inlined in both examples, and the call to it replaced by the constant 351. Oh well, at least the compiler got rid of some useless code in the second case, and the binary will be smaller.

The trick

The trick I came up with is the following:

#ifdef TEST
#define STATIC
#else
#define STATIC static
#endif

Now put the snippet above at the top of the C file where the functions you want to test are implemented and declare your functions as STATIC. When you compile your code normally, these functions will be compiled as static, but if you use the -DTEST option, STATIC will expand to nothing and the functions will be visible outside the file.

Here is a complete example.

foo4.c:

#include <stdio.h>

#ifdef TEST
#define STATIC
#else
#define STATIC static
#endif

STATIC int foo(int x, int y)
{
    return 42*x - 69*y;
}

test4.c

#include <stdio.h>

int foo(int, int);

int main() {
    int result = foo(1, 1);

    if (result == -27) {
        fprintf(stderr, "Test passed\n");
        return 0;
    } else {
        fprintf(stderr, "Test failed: expected -27, got %d\n", result);
        return 1;
    }
}

You can download the source files (links above) and try for yourself: build with gcc foo4.c test4.c and you’ll get a linker error undefined symbol: foo; build with gcc -DTEST foo4.c test4.c and run ./a.out to see the test pass!

A few days before coming up with this trick, I had learned about a similar use of C macros useful for debugging purposes. I wanted to have some extra logging to be enabled only when I chose so, for example when using a -DDEBUG option. What I used to do was throwing #ifdefs all over my codebase, like this:

    if (flob < 0) {
#ifdef DEBUG
        fprintf(stderr, "Invalid value for flob: %d\n", flob);
#endif
        return -1;
    }

But what I have found (on the Wikipedia page on the C preprocessor) is that you can use a single #ifdef at the top of your file:

#ifdef DEBUG
#define DBG_LOG(...) fprintf(stderr, __VA_ARGS__)
#else
#define DBG_LOG(...)
#endif

/* More code ... */

    if (flob < 0) {
        DBG_LOG("Invalid value for flob: %d\n", flob);
        return -1;
    }

Here I am using a variadic macro, which is supported in C99 but not, as far as I know, in C89. If you want to try this out, you’ll have to build with -std=c99 or a similar option.

Sometimes the part I want to conditionally compile is not just the information logging, but the whole conditional expression. To do this, I actually use something like this in my code:

#ifdef DEBUG
#define DBG_ASSERT(condition, value, ...)     \
    if (!(condition)) {                   \
        fprintf(stderr, __VA_ARGS__); \
        return value;                 \
    }
#else
#define DBG_ASSERT(...)
#endif

/* More code ... */

    DBG_ASSERT(flob >= 0, -1, "Invalid value for flob: %d\n", flob);

Here condition can be any C expression. Macros are powerful!

Conclusion

Depending on your taste, you may find this a clean way to write C code, or a disgusting hack that should never be used.

If you are working on a project where you can choose your own coding style, I encourage you to try out tricks like this and see for yourself if you like them or not. In the worst case, you’ll make mistakes and learn what not to do next time!