aboutsummaryrefslogtreecommitdiff
path: root/src/blog
diff options
context:
space:
mode:
authorSebastiano Tronto <sebastiano@tronto.net>2024-06-20 15:40:29 +0200
committerSebastiano Tronto <sebastiano@tronto.net>2024-06-20 15:40:29 +0200
commita3f35dbc0419ecb5fa3f346d34a90a3a5116acc3 (patch)
treecd66fd094a5ff7ec2ee8a0f28faefe1c411c5889 /src/blog
parent1bcb4bfa744d0fe9c341724405b8344a97a402de (diff)
downloadsebastiano.tronto.net-a3f35dbc0419ecb5fa3f346d34a90a3a5116acc3.tar.gz
sebastiano.tronto.net-a3f35dbc0419ecb5fa3f346d34a90a3a5116acc3.zip
Added blog post
Diffstat (limited to 'src/blog')
-rw-r--r--src/blog/2024-06-20-callback-log/callback-log.md197
1 files changed, 197 insertions, 0 deletions
diff --git a/src/blog/2024-06-20-callback-log/callback-log.md b/src/blog/2024-06-20-callback-log/callback-log.md
new file mode 100644
index 0000000..8b2c87d
--- /dev/null
+++ b/src/blog/2024-06-20-callback-log/callback-log.md
@@ -0,0 +1,197 @@
1# Another C trick: flexible logging with callback functions
2
3Working with C on my personal projects I enjoy complete freedom: I
4don't have to follow existing conventions, I don't have to bend my
5code to fit into an existing codebase, I can re-organize the program's
6structure whenever I feel like. On the other hand, I don't get to
7learn well-known good practices that were invented by whose who
8beat their heads against the wall before me.
9
10Sometimes, through a lot of wall-headbanging, I get to re-invent
11these good practices by myself. Some time ago I wrote about
12[using macros to make functions testable](../2023-11-14-test-visibility-c-macro).
13Now it is time for another trick: using a
14[callback function](https://en.wikipedia.org/wiki/Callback_(computer_programming))
15for logging!
16
17## The problem
18
19I am working on a new version of [nissy](https://nissy.tronto.net),
20a Rubik's cube solver with some extra functionalities that I and a
21few others use. The code is public but I won't link it here for
22now; it is not ready and many things will likely change by the time
23you read this post.
24
25One thing I decided to do was organizing the code as a library that
26can be used by other programs to provide user-facing functionalities.
27In other words, the main code does not have a `main()` function.
28Other programs that use this code as a library include or may include
29in the future:
30
31* A testing utility / framework
32* A command line interface
33* A graphical interface (tentative)
34
35As a consequence, the library itself should avoid using standard
36input and output - that would be fine for the CLI interface, but
37useless for the GUI. All the necessary information is exchanged
38between the library and the client code via function return values
39and parameters, including `char *` buffers for text output.
40This also means I can get rid of `stdio.h` from my library code.
41
42However, sometimes it is useful to see some logging info, especially
43when debugging. For this reason, I have hidden an `#include <stdio.h>`
44behind the compile-time option `#ifdef DEBUG`.
45
46This means so far I could only see log information when building
47in debug mode. But I have recently realized that this is too limiting:
48even in the user-facing version of this program, long-running queries
49could benefit from more frequent output before the response has been
50calculated. But how can I do this without using `stdio`?
51
52## Brainstorming
53
54Problem solving is full of dead ends and wasted brain-cycles. Before
55I present to you the clever solution I found like I am some kind
56of coding guru, let's see some of the bad ideas that I came up with
57and discarded.
58
59### More macros
60
61If I can hide debug logging behind a compile-time macro, why not
62doing it for logging in general? I could split up the logging from
63the other debug stuff and let the programmer (i.e. me) choose at
64build time whether to have logs or not.
65
66But this is not great. First of all, I don't like the idea of making
67the build system more complex, I think it makes the code less
68portable. Secondly, it is less flexible, because it limits the
69options to what I decide right now. Of course I could add more stuff
70later as needed, but this implies more coupling between the library
71and the client.
72
73### Use `char *` buffers
74
75Since some information is exchanged via `char *` output parameters,
76the same could be done for logging information. This is very flexible,
77because it allows the caller to do whatever they want with the log
78output. But passing a "log buffer" parameter to every function call,
79which is likely to be the same for the whole program, is annoying
80and feels redundant. Moreover, reading this information off the the
81buffer in real time is very complicated.
82
83### Custom `FILE *` stream
84
85If I log the information to
86[standard error](https://en.wikipedia.org/wiki/Stderr) using
87`fprintf(stderr, ...)`, perhaps replacing `stderr` with a custom
88`FILE *` object could do the trick. It limits the log messages to
89be written to a file, but in the UNIX world *almost* everything
90is a file. This trick may not be the most portable, but it is
91certainly quite flexible.
92
93But how would the programmer set this custom stream? There must be
94a way to do it with macros, but then we have the same problems of
95the first idea. Alternatively, I could define a global `FILE *`
96variable and add to my interface to let the caller set this variable...
97
98And this brought me to the final idea: if I can set a global variable
99via a function call, why not make this variable a pointer to a
100function?
101
102## The solution
103
104The solution I settled for is the following.
105
106In the main source file `mylib.c` I define a global function pointer
107
108```
109void (*mylib_log)(const char *, ...);
110```
111
112This pointer will be used to call the function that prints the log.
113By default it is unset (NULL), which I choose to interpret as "no
114logs should be printed". I used the dots `...` because I wanted
115this function to be
116[variadic](https://en.wikipedia.org/wiki/Variadic_function), just
117like the classic `printf()`.
118
119After this declaration I have some wrapper code that checks if the
120logger function is set before calling it:
121
122```
123#define LOG(...) if (mylib_log != NULL) mylib_log(__VA_ARGS__);
124```
125
126The macro above uses `__VA_ARGS__` to refer to the list of arguments
127denoted by the three dots. Now to show a log message I can do:
128
129```
130void do_thing(int a, int b) {
131 int x = compute(a, b);
132
133 LOG("Computed value %d\n", x)
134
135 return x;
136}
137```
138
139Finally, there is a public function that lets the user of the
140library set the logger function:
141
142```
143void mylib_setlogger(void (*f)(const char *, ...)) {
144 mylib_log = f;
145}
146```
147
148And that's it! Using this setup is pretty simple. For example, for my unit
149tests I have something like this:
150
151```
152#include <stdarg.h> /* For va_list and related functions */
153
154void log_stderr(const char *str, ...)
155{
156 va_list args;
157
158 va_start(args, str);
159 vfprintf(stderr, str, args);
160 va_end(args);
161}
162
163int main(void) {
164 mylib_setlogger(log_stderr);
165 run_test();
166 return 0;
167}
168```
169
170The function `vfprintf()` is a version of `fprintf()` that takes a
171[`va_list`](https://en.cppreference.com/w/c/variadic/va_list)
172argument instead of an old-style parameter list. This is the
173standard way to pass on variadic function arguments, as far as I
174know. In case you did not know, `fprintf()` is a version of
175`printf()` that prints to a given stream - in this case, `stderr`.
176
177Now if I ever get to implementing a GUI, all I have to do to show
178`mylib`'s logs is implementing a function `log_to_gui()` that shows
179the given text somewhere, and call a `mylib_setlogger(log_to_gui)`
180at the start of the program. Neat!
181
182## Conclusion
183
184When I code for fun, I definitely enjoy coming up with my own
185solution for problems like this, even if the solution already exists
186somewhere. I also think that re-inventing standard practices like
187this is making me a better programmer, because after struggling
188with it myself I understand the practices better and I appreciate
189them more.
190
191This time in particular I learnt how to implement variadic functions,
192and a bit of their
193[history](https://stackoverflow.com/questions/14082476/what-is-the-best-way-for-giving-callback-for-logging).
194I am still not sure if this will work well when I end up using
195this library in a non-C project. Is it even possible to call C
196variadic functions from other languages? Will I have to provide a
197logger that takes a `va_list` instead? I guess I'll find out!

Generated with cgit - Back to sebastiano.tronto.net