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!