sebastiano.tronto.net

Source files and build scripts for my personal website
git clone https://git.tronto.net/sebastiano.tronto.net
Download | Log | Files | Refs | README

test-visibility-c-macro.md (7369B)


      1 # Making functions public for tests only... with C macros!
      2 
      3 As a programmer, I often face this dilemma: should I make this
      4 function private to improve encapsulation, or should I make it
      5 public so that I can write tests for it? I believe this problem is
      6 especially felt in scientific computing, or when implementing big,
      7 complex algorithms whose many small substeps have no place in a
      8 public interface, but should be unit-tested anyway.
      9 
     10 Until recently, I essentially had two ways to deal with this (with
     11 a strong preference for the first one):
     12 
     13 * Make the function public, tests are important. Who cares about visibility.
     14 * Make the function private and skip the tests. Errors will be caught when
     15   testing the higher-level routine that calls this smaller function.
     16 
     17 But a few days ago I thought of a cool trick (that realistically
     18 has been known for at least 45 years, I just was not aware of it
     19 before) to solve this problem for my C projects, using conditional
     20 compilation.  Let's dive in!
     21 
     22 ## Function visibility in C
     23 
     24 By default, functions in C are "public", by which I mean visible to any other
     25 *[translation unit](https://en.wikipedia.org/wiki/Translation_unit_%28programming%29)*
     26 (file). For example, say you have the following files:
     27 
     28 `foo.c`:
     29 
     30 ```
     31 int foo(int x, int y) {
     32 	return 42*x - 69*y;
     33 }
     34 ```
     35 
     36 `main.c`:
     37 
     38 ```
     39 #include <stdio.h>
     40 
     41 int foo(int, int); // Function prototype
     42 
     43 int main() {
     44 	int z = foo(10, 1);
     45 	printf("%d\n", z);
     46 	return 0;
     47 }
     48 ```
     49 
     50 You can build them with `gcc foo.c main.c`, and the program will
     51 run correctly and output `351`. Usually, the function prototype is
     52 put in a separate `foo.h` file and it is included in `main.c` with
     53 `#include "foo.h"`.
     54 
     55 This works because a C program is built into an executable in two
     56 steps: *[compiling](https://en.wikipedia.org/wiki/Compiler)* and
     57 *[linking](https://en.wikipedia.org/wiki/Linker_(computing))*.
     58 During the first of these two, each file is translated into
     59 *[object code](https://en.wikipedia.org/wiki/Object_file)*; if the
     60 compiler finds a reference to a function whose body is not present in
     61 the same file - like our `foo()` in `main.c` - it does not complain,
     62 but it trusts the programmer that this function is implemented somewhere
     63 else. Then it is the turn of the linker, whose job is exactly is to
     64 put together the object files and resolve these function calls;
     65 the linker *does* complain if the body of `foo()` is nowhere to be found.
     66 
     67 All of this is different for functions marked as `static`. These are
     68 only visible inside the file where they are defined.
     69 
     70 ## Why make functions `static`?
     71 
     72 There are a couple of reasons why one should make (some) functions
     73 `static`:
     74 
     75 * As a hint to other programmers: similarly to the `private` modifier
     76   in object oriented languages, `static` immediately communicates that
     77   this function is only used locally, and will not be called from other
     78   modules. It also prevents someone from calling it from another file
     79   by mistake.
     80 * As a hint to the compiler: if a compiler sees a `static` function, it
     81   knows all the places where this function is called, and it can
     82   choose to optimize out all the
     83   [assembly boilerplate](https://en.wikipedia.org/wiki/Calling_convention)
     84   related to function calls and
     85   [inline it](https://en.wikipedia.org/wiki/Inline_expansion).
     86 
     87 To illustrate the second point, I have put all the code of the
     88 previous example in the same file [`main2.c`](./main2.c). You can
     89 compile it with `gcc -O1 -S main2.c` to enable optimizations and
     90 generate the assembly code instead of an exectuable. I have uploaded
     91 the output here: [`main2.s`](./main2.s). Then you can do the same with
     92 [`main3.c`](./main3.c), whose only difference is that `foo()` is now
     93 static, and check the resulting [`main3.s`](./main3.s).
     94 
     95 As you can see, the section labelled `foo:` has disappeared. This
     96 is because the compiler knows that it will not be needed anywhere
     97 else; it inlined it everywhere it saw a reference to it and called
     98 it a day.
     99 
    100 You may also see that `foo` was actually inlined in *both* examples,
    101 and the call to it replaced by the constant `351`. Oh well, at least
    102 the compiler got rid of some useless code in the second case, and
    103 the binary will be smaller.
    104 
    105 ## The trick
    106 
    107 The trick I came up with is the following:
    108 
    109 ```
    110 #ifdef TEST
    111 #define _static
    112 #else
    113 #define _static static
    114 #endif
    115 ```
    116 
    117 Now put the snippet above at the top of the C file where the functions
    118 you want to test are implemented and declare your functions as
    119 `_static` with an underscore. When you compile your code normally,
    120 these functions will be compiled as `static`, but if you use the
    121 `-DTEST` option, `_static` will expand to nothing and the functions
    122 will be visible outside the file.
    123 
    124 Here is a complete example.
    125 
    126 [`foo4.c`](./foo4.c):
    127 
    128 ```
    129 #include <stdio.h>
    130 
    131 #ifdef TEST
    132 #define _static
    133 #else
    134 #define _static static
    135 #endif
    136 
    137 _static int foo(int x, int y)
    138 {
    139 	return 42*x - 69*y;
    140 }
    141 ```
    142 
    143 [`test4.c`](./test4.c)
    144 
    145 ```
    146 #include <stdio.h>
    147 
    148 int foo(int, int);
    149 
    150 int main() {
    151 	int result = foo(1, 1);
    152 
    153 	if (result == -27) {
    154 		fprintf(stderr, "Test passed\n");
    155 		return 0;
    156 	} else {
    157 		fprintf(stderr, "Test failed: expected -27, got %d\n", result);
    158 		return 1;
    159 	}
    160 }
    161 ```
    162 
    163 You can download the source files (links above) and try for yourself:
    164 build with `gcc foo4.c test4.c` and you'll get a linker error
    165 `undefined symbol: foo`; build with `gcc -DTEST foo4.c test4.c` and
    166 run `./a.out` to see the test pass!
    167 
    168 ## Related tricks
    169 
    170 A few days before coming up with this trick, I had learned about a
    171 similar use of C macros useful for debugging purposes. I wanted to
    172 have some extra logging to be enabled only when I chose so, for
    173 example when using a `-DDEBUG` option. What I used to do was throwing
    174 `#ifdef`s all over my codebase, like this:
    175 
    176 ```
    177 	if (flob < 0) {
    178 #ifdef DEBUG
    179 		fprintf(stderr, "Invalid value for flob: %d\n", flob);
    180 #endif
    181 		return -1;
    182 	}
    183 ```
    184 
    185 But what I have found (on the
    186 [Wikipedia page on the C preprocessor](https://en.wikipedia.org/wiki/C_preprocessor))
    187 is that you can use a single `#ifdef` at the top of your file:
    188 
    189 ```
    190 #ifdef DEBUG
    191 #define DBG_LOG(...) fprintf(stderr, __VA_ARGS__)
    192 #else
    193 #define DBG_LOG(...)
    194 #endif
    195 
    196 /* More code ... */
    197 
    198 	if (flob < 0) {
    199 		DBG_LOG("Invalid value for flob: %d\n", flob);
    200 		return -1;
    201 	}
    202 ```
    203 
    204 Here I am using a *variadic macro*, which is supported in C99 but not,
    205 as far as I know, in C89. If you want to try this out, you'll have to
    206 build with `-std=c99` or a similar option.
    207 
    208 Sometimes the part I want to conditionally compile is not just the
    209 information logging, but the whole conditional expression. To do this,
    210 I actually use something like this in my code:
    211 
    212 ```
    213 #ifdef DEBUG
    214 #define DBG_ASSERT(condition, value, ...)     \
    215 	if (!(condition)) {                   \
    216 		fprintf(stderr, __VA_ARGS__); \
    217 		return value;                 \
    218 	}
    219 #else
    220 #define DBG_ASSERT(...)
    221 #endif
    222 
    223 /* More code ... */
    224 
    225 	DBG_ASSERT(flob >= 0, -1, "Invalid value for flob: %d\n", flob);
    226 ```
    227 
    228 Here `condition` can be any C expression. Macros are powerful!
    229 
    230 ## Conclusion
    231 
    232 Depending on your taste, you may find this a clean way to write
    233 C code, or a disgusting hack that should never be used.
    234 
    235 If you are working on a project where you can choose your own coding
    236 style, I encourage you to try out tricks like this and see for
    237 yourself if you like them or not. In the worst case, you'll make
    238 mistakes and learn what *not* to do next time!