aboutsummaryrefslogtreecommitdiff
path: root/src/blog
diff options
context:
space:
mode:
authorSebastiano Tronto <sebastiano@tronto.net>2023-11-14 18:49:34 +0100
committerSebastiano Tronto <sebastiano@tronto.net>2023-11-14 18:49:34 +0100
commitce25bbd863904ac7784b647b6b8e22196e7674ff (patch)
treee4e11995999db844bee50692e63d5c117af3e2f9 /src/blog
parent5ed505e280c6c6a7459bb2e4249313fa65df052f (diff)
downloadsebastiano.tronto.net-ce25bbd863904ac7784b647b6b8e22196e7674ff.tar.gz
sebastiano.tronto.net-ce25bbd863904ac7784b647b6b8e22196e7674ff.zip
Added blog post
Diffstat (limited to 'src/blog')
-rw-r--r--src/blog/2023-11-14-test-visibility-c-macro/foo4.c15
-rw-r--r--src/blog/2023-11-14-test-visibility-c-macro/main2.c12
-rw-r--r--src/blog/2023-11-14-test-visibility-c-macro/main2.s45
-rw-r--r--src/blog/2023-11-14-test-visibility-c-macro/main3.c12
-rw-r--r--src/blog/2023-11-14-test-visibility-c-macro/main3.s26
-rw-r--r--src/blog/2023-11-14-test-visibility-c-macro/test-visibility-c-macro.md238
-rw-r--r--src/blog/2023-11-14-test-visibility-c-macro/test4.c15
7 files changed, 363 insertions, 0 deletions
diff --git a/src/blog/2023-11-14-test-visibility-c-macro/foo4.c b/src/blog/2023-11-14-test-visibility-c-macro/foo4.c
new file mode 100644
index 0000000..85dea34
--- /dev/null
+++ b/src/blog/2023-11-14-test-visibility-c-macro/foo4.c
@@ -0,0 +1,15 @@
1#include <stdio.h>
2
3/* cc -DTEST main4.c # foo is visible */
4/* cc main4.c # foo is static */
5
6#ifdef TEST
7#define _static
8#else
9#define _static static
10#endif
11
12_static int foo(int x, int y)
13{
14 return 42*x - 69*y;
15}
diff --git a/src/blog/2023-11-14-test-visibility-c-macro/main2.c b/src/blog/2023-11-14-test-visibility-c-macro/main2.c
new file mode 100644
index 0000000..b7c3ef1
--- /dev/null
+++ b/src/blog/2023-11-14-test-visibility-c-macro/main2.c
@@ -0,0 +1,12 @@
1#include <stdio.h>
2
3int foo(int x, int y)
4{
5 return 42*x - 69*y;
6}
7
8int main() {
9 int z = foo(10, 1);
10 printf("%d\n", z);
11 return 0;
12}
diff --git a/src/blog/2023-11-14-test-visibility-c-macro/main2.s b/src/blog/2023-11-14-test-visibility-c-macro/main2.s
new file mode 100644
index 0000000..a3f580e
--- /dev/null
+++ b/src/blog/2023-11-14-test-visibility-c-macro/main2.s
@@ -0,0 +1,45 @@
1 .text
2 .file "main2.c"
3 .globl foo # -- Begin function foo
4 .p2align 4, 0x90
5 .type foo,@function
6foo: # @foo
7 .cfi_startproc
8# %bb.0:
9 imull $42, %edi, %ecx
10 imull $-69, %esi, %eax
11 addl %ecx, %eax
12 retq
13.Lfunc_end0:
14 .size foo, .Lfunc_end0-foo
15 .cfi_endproc
16 # -- End function
17 .globl main # -- Begin function main
18 .p2align 4, 0x90
19 .type main,@function
20main: # @main
21 .cfi_startproc
22# %bb.0:
23 pushq %rax
24 .cfi_def_cfa_offset 16
25 leaq .L.str(%rip), %rdi
26 movl $351, %esi # imm = 0x15F
27 xorl %eax, %eax
28 callq printf@PLT
29 xorl %eax, %eax
30 popq %rcx
31 .cfi_def_cfa_offset 8
32 retq
33.Lfunc_end1:
34 .size main, .Lfunc_end1-main
35 .cfi_endproc
36 # -- End function
37 .type .L.str,@object # @.str
38 .section .rodata.str1.1,"aMS",@progbits,1
39.L.str:
40 .asciz "%d\n"
41 .size .L.str, 4
42
43 .ident "clang version 15.0.7"
44 .section ".note.GNU-stack","",@progbits
45 .addrsig
diff --git a/src/blog/2023-11-14-test-visibility-c-macro/main3.c b/src/blog/2023-11-14-test-visibility-c-macro/main3.c
new file mode 100644
index 0000000..c01779a
--- /dev/null
+++ b/src/blog/2023-11-14-test-visibility-c-macro/main3.c
@@ -0,0 +1,12 @@
1#include <stdio.h>
2
3static int foo(int x, int y)
4{
5 return 42*x - 69*y;
6}
7
8int main() {
9 int z = foo(10, 1);
10 printf("%d\n", z);
11 return 0;
12}
diff --git a/src/blog/2023-11-14-test-visibility-c-macro/main3.s b/src/blog/2023-11-14-test-visibility-c-macro/main3.s
new file mode 100644
index 0000000..6b7d9f9
--- /dev/null
+++ b/src/blog/2023-11-14-test-visibility-c-macro/main3.s
@@ -0,0 +1,26 @@
1 .file "main3.c"
2 .text
3 .section .rodata.str1.1,"aMS",@progbits,1
4.LC0:
5 .string "%d\n"
6 .text
7 .globl main
8 .type main, @function
9main:
10.LFB12:
11 .cfi_startproc
12 subq $8, %rsp
13 .cfi_def_cfa_offset 16
14 movl $351, %esi
15 leaq .LC0(%rip), %rdi
16 movl $0, %eax
17 call printf@PLT
18 movl $0, %eax
19 addq $8, %rsp
20 .cfi_def_cfa_offset 8
21 ret
22 .cfi_endproc
23.LFE12:
24 .size main, .-main
25 .ident "GCC: (GNU) 12.2.0"
26 .section .note.GNU-stack,"",@progbits
diff --git a/src/blog/2023-11-14-test-visibility-c-macro/test-visibility-c-macro.md b/src/blog/2023-11-14-test-visibility-c-macro/test-visibility-c-macro.md
new file mode 100644
index 0000000..ffc9956
--- /dev/null
+++ b/src/blog/2023-11-14-test-visibility-c-macro/test-visibility-c-macro.md
@@ -0,0 +1,238 @@
1# Making functions public for tests only... with C macros!
2
3As a programmer, I often face this dilemma: should I make this
4function private to improve encapsulation, or should I make it
5public so that I can write tests for it? I believe this problem is
6especially felt in scientific computing, or when implementing big,
7complex algorithms whose many small substeps have no place in a
8public interface, but should be unit-tested anyway.
9
10Until recently, I essentially had two ways to deal with this (with
11a 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
17But a few days ago I thought of a cool trick (that realistically
18has been known for at least 45 years, I just was not aware of it
19before) to solve this problem for my C projects, using conditional
20compilation. Let's dive in!
21
22## Function visibility in C
23
24By 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```
31int foo(int x, int y) {
32 return 42*x - 69*y;
33}
34```
35
36`main.c`:
37
38```
39#include <stdio.h>
40
41int foo(int, int); // Function prototype
42
43int main() {
44 int z = foo(10, 1);
45 printf("%d\n", z);
46 return 0;
47}
48```
49
50You can build them with `gcc foo.c main.c`, and the program will
51run correctly and output `351`. Usually, the function prototype is
52put in a separate `foo.h` file and it is included in `main.c` with
53`#include "foo.h"`.
54
55This works because a C program is built into an executable in two
56steps: *[compiling](https://en.wikipedia.org/wiki/Compiler)* and
57*[linking](https://en.wikipedia.org/wiki/Linker_(computing))*.
58During the first of these two, each file is translated into
59*[object code](https://en.wikipedia.org/wiki/Object_file)*; if the
60compiler finds a reference to a function whose body is not present in
61the same file - like our `foo()` in `main.c` - it does not complain,
62but it trusts the programmer that this function is implemented somewhere
63else. Then it is the turn of the linker, whose job is exactly is to
64put together the object files and resolve these function calls;
65the linker *does* complain if the body of `foo()` is nowhere to be found.
66
67All of this is different for functions marked as `static`. These are
68only visible inside the file where they are defined.
69
70## Why make functions `static`?
71
72There 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
87To illustrate the second point, I have put all the code of the
88previous example in the same file [`main2.c`](./main2.c). You can
89compile it with `gcc -O1 -S main2.c` to enable optimizations and
90generate the assembly code instead of an exectuable. I have uploaded
91the 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
93static, and check the resulting [`main3.s`](./main3.s).
94
95As you can see, the section labelled `foo:` has disappeared. This
96is because the compiler knows that it will not be needed anywhere
97else; it inlined it everywhere it saw a reference to it and called
98it a day.
99
100You may also see that `foo` was actually inlined in *both* examples,
101and the call to it replaced by the constant `351`. Oh well, at least
102the compiler got rid of some useless code in the second case, and
103the binary will be smaller.
104
105## The trick
106
107The 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
117Now put the snippet above at the top of the C file where the functions
118you want to test are implemented and declare your functions as
119`_static` with an underscore. When you compile your code normally,
120these functions will be compiled as `static`, but if you use the
121`-DTEST` option, `_static` will expand to nothing and the functions
122will be visible outside the file.
123
124Here 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
148int foo(int, int);
149
150int 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
163You can download the source files (links above) and try for yourself:
164build 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
166run `./a.out` to see the test pass!
167
168## Related tricks
169
170A few days before coming up with this trick, I had learned about a
171similar use of C macros useful for debugging purposes. I wanted to
172have some extra logging to be enabled only when I chose so, for
173example 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
185But what I have found (on the
186[Wikipedia page on the C preprocessor](https://en.wikipedia.org/wiki/C_preprocessor))
187is 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
204Here I am using a *variadic macro*, which is supported in C99 but not,
205as far as I know, in C89. If you want to try this out, you'll have to
206build with `-std=c99` or a similar option.
207
208Sometimes the part I want to conditionally compile is not just the
209information logging, but the whole conditional expression. To do this,
210I 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
228Here `condition` can be any C expression. Macros are powerful!
229
230## Conclusion
231
232Depending on your taste, you may find this a clean way to write
233C code, or a disgusting hack that should never be used.
234
235If you are working on a project where you can choose your own coding
236style, I encourage you to try out tricks like this and see for
237yourself if you like them or not. In the worst case, you'll make
238mistakes and learn what *not* to do next time!
diff --git a/src/blog/2023-11-14-test-visibility-c-macro/test4.c b/src/blog/2023-11-14-test-visibility-c-macro/test4.c
new file mode 100644
index 0000000..9298e89
--- /dev/null
+++ b/src/blog/2023-11-14-test-visibility-c-macro/test4.c
@@ -0,0 +1,15 @@
1#include <stdio.h>
2
3int foo(int, int);
4
5int main() {
6 int result = foo(1, 1);
7
8 if (result == -27) {
9 fprintf(stderr, "Test passed\n");
10 return 0;
11 } else {
12 fprintf(stderr, "Test failed: expected -27, got %d\n", result);
13 return 1;
14 }
15}

Generated with cgit - Back to sebastiano.tronto.net