aboutsummaryrefslogtreecommitdiff
path: root/src/blog
diff options
context:
space:
mode:
authorSebastiano Tronto <sebastiano@tronto.net>2025-04-05 11:06:21 +0200
committerSebastiano Tronto <sebastiano@tronto.net>2025-04-05 11:06:21 +0200
commit9ee63bff4c8c631e1cb69944ed49e64a9f91f94c (patch)
tree39a53127aee617678daaa2d6e140354f05b3a16e /src/blog
parent7219836b03f169f9ea69e4dfdf3f35dfdba80ab0 (diff)
downloadsebastiano.tronto.net-9ee63bff4c8c631e1cb69944ed49e64a9f91f94c.tar.gz
sebastiano.tronto.net-9ee63bff4c8c631e1cb69944ed49e64a9f91f94c.zip
Blog post
Diffstat (limited to 'src/blog')
-rw-r--r--src/blog/2025-04-05-learnt-rewrite/learned-rewrite.md444
1 files changed, 444 insertions, 0 deletions
diff --git a/src/blog/2025-04-05-learnt-rewrite/learned-rewrite.md b/src/blog/2025-04-05-learnt-rewrite/learned-rewrite.md
new file mode 100644
index 0000000..be9a784
--- /dev/null
+++ b/src/blog/2025-04-05-learnt-rewrite/learned-rewrite.md
@@ -0,0 +1,444 @@
1# Things I learned rewriting a project from scratch
2
3About two years ago I [wrote](../2023-04-10-the-big-rewrite) about a
4project I have been working on and off for a few years. In that post
5I explain how and why I decided to rewrite it from scracth, splitting
6it in multiple parts. I ended up not following exactly the plan that I
7laid out, and I also worked on the rewrite more slowly than I wanted.
8But rewriting from scratch allowed me to experiment with new ideas and
9discover new tricks, and after two years I learned enough stuff that I
10can make a blog post about it!
11
12The project I am talking about is [Nissy](https://nissy.tronto.net), a
13command-line Rubik's Cube solver that is quite popular in the very small
14niche of speedcubers interested in the *Fewest Moves Challenge*, which
15consists in solving the puzzle with the smallest amount of moves rather
16than as fast as possible. Both the old and the new versions are written
17in C, but not all the things I discuss in this post are language-specific.
18
19You can find the new version of this project, the "Big Rewrite",
20in [this repository](https://git.tronto.net/h48) - or
21[on github](https://github.com/sebastianotronto/h48), if you prefer.
22I decided to rename it "H48" back when I planned to have separate projects
23for the different features of the old version, but I may change it back
24to Nissy at some point.
25
26But now let's get into it!
27
28## Better memory safety (yes, even in C)
29
30Let's start with a mistake that I think many inexperienced C programmers
31make: in the previous iterations of this project, I used a lot of
32`malloc()`s. And although I was quite diligent in always freeing the
33memory I used, in most cases I would have been better off not using
34dynamic memory allocation at all.
35
36As an example, let's consider the case of converting some data structure
37into a string. One way to do it would be something like this:
38
39```
40char *mytype_to_string(const mytype_t *x) {
41 const size_t str_size = 512;
42 char *str = malloc(str_size);
43
44 /* Write stuff to str here */
45
46 return str;
47}
48```
49
50If you are familiar with
51[garbage-collected languages](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)),
52you may find the code above nice and clean. However, since with C we have
53to manage our memory manually, the caller of this function would have
54to remember to `free()` the pointer returned by `my_type_to_string()`.
55This is not rocket science, but it is a common source of mistakes.
56
57However, a safer pattern for this kind of function would be the following:
58
59```
60int mytype_to_string(const mytype_t *x, size_t len, char buffer[len]) {
61 const size_t str_size = 512;
62 if (len < str_size) {
63 /* Handle error in some way, for example by returning -1 */
64 }
65
66 /* Write stuff to buffer here */
67
68 return 0; /* Or return number of bytes written */
69}
70```
71
72In this new version of the code, instead of returning a string, the
73function takes a `char` buffer and its lenght as a parameter. Notice
74that we are **not** passing the buffer by value, as the notation
75`buffer[len]` might suggest: this function's signature is equivalent to
76`my_type_to_string(const mytype_t *, size_t, char *)`, but by using the
77square-bracket notation the compiler may be able to catch a too-small
78buffer and warn us about it.
79
80Now we can get rid of the malloc entirely, because the caller can
81just allocate the char buffer on its stack:
82
83```
84void doing_stuff(mytype_t x) {
85 const size_t len = 1024;
86 char str[len];
87 if (mytype_to_string(&x, len, str) != 0) {
88 /* Handle error */
89 }
90
91 /* ... */
92}
93```
94
95Unfortuntely, this new pattern leads to a small problem: the caller
96must now be aware of the number of characters required for converting
97`mytype_t` to a string. There are a couple of ways to solve this problem,
98including handling a `NULL` first parameter as a "dry-run" request that
99returns the required size without writing anything, or using a global
100constant.
101
102This brings me to another feature I have started using more and more
103recently. Say we want to use a global constant for the size of `mytype_t`.
104We could do something like this:
105
106```
107#define MYTYPE_SIZE 512
108
109int mytype_to_string(const mytype_t *x, char buffer[static MYTYPE_SIZE]) {
110 /* Write to buffer here, no error handling needed */
111
112 return 0; /* Or return number of bytes written */
113}
114
115void doing_stuff(mytype_t x) {
116 char str[MYTYPE_SIZE];
117 mytype_to_string(&x, str);
118
119 /* ... */
120}
121```
122
123Notice how I used `[static MYTYPE_SIZE]` to denote the required size
124of the buffer parameter. This tells the compiler that the function
125`mytype_to_string()` can assume that the buffer must reference an area
126of memory of size at least `MYTYPE_SIZE`. In turn, the compiler can
127use this information not only to optimize the code, but also to give
128useful warnings. This trick can also be used to tell the compiler that
129a pointer cannot be `NULL`: a pointer is not `NULL` if it refers to an
130valid area of memory of size at least 1. So we can declare a function
131as follows:
132
133```
134int mytype_to_string(const mtytype_t x[static 1], char buffer[static MYTYPE_SIZE]) {
135 /* ... */
136}
137```
138
139With this we can come up with a set of rules to never de-reference a
140NULL pointer. A pointer must be either:
141
1421. Obtained by de-referencing an object allocated in current function.
1432. A parameter declared as `[static 1]`.
1443. Checked for `NULL` before de-referencing.
145
146Having more memory safety baked into the language would be more convenient,
147but at least we can use some mitigations.
148
149## Sanitizers
150
151Another tool that help me improve the memory management of my code are
152[sanitizers](https://en.wikipedia.org/wiki/Code_sanitizer).
153They were suggested to me by [Tomas Rokicki](https://tomas.rokicki.com)
154when I shared my struggles with
155[an old bug](../2023-05-05-debug-smartphone), and now that I got used
156to them I am wondering how I even survived programming in C without them.
157Sanitizers are *compiler intrumentations* supported by the major C and
158C++ compilers - namely GCC and Clang. They add some runtime checks to
159a program that make it crash with a clear message if they detect
160a memory error, such as an access outside the bounds of an array, or a
161race condition. For example, say you try to access some out-of-bound
162element of an array:
163
164```
165/* This is a.c */
166
167#include <stdio.h>
168
169int main() {
170 int a[4] = {0, 1, 2, 3};
171
172 printf("Out-of-bound value: %d\n", a[4]);
173}
174```
175
176Normally, this program just runs fine and prints some garbage value:
177
178```
179$ cc a.c && ./a.out
180Out-of-bound value: 1573265008
181```
182
183Notice how the compiler did not even give us a warning!
184However, if we compile with the address sanitizer enabled:
185
186```
187$ gcc -fsanitize=address a.c
188$ ./a.out
189=================================================================
190==15152==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fc4e2500030 at pc 0x000000401331 bp 0x7ffdffd953c0 sp 0x7ffdffd953b8
191READ of size 4 at 0x7fc4e2500030 thread T0
192 #0 0x401330 in main (/home/sebastiano/a.out+0x401330) (BuildId: e8411facf942864956c817b9be7bee9868ec6065)
193 #1 0x7fc4e4a10247 in __libc_start_call_main (/lib64/libc.so.6+0x3247) (BuildId: f83d43b9b4b0ed5c2bd0a1613bf33e08ee054c93)
194 #2 0x7fc4e4a1030a in __libc_start_main_alias_1 (/lib64/libc.so.6+0x330a) (BuildId: f83d43b9b4b0ed5c2bd0a1613bf33e08ee054c93)
195 #3 0x4010d4 in _start (/home/sebastiano/a.out+0x4010d4) (BuildId: e8411facf942864956c817b9be7bee9868ec6065)
196
197Address 0x7fc4e2500030 is located in stack of thread T0 at offset 48 in frame
198 #0 0x4011a5 in main (/home/sebastiano/a.out+0x4011a5) (BuildId: e8411facf942864956c817b9be7bee9868ec6065)
199
200 This frame has 1 object(s):
201 [32, 48) 'a' (line 4) <== Memory access at offset 48 overflows this variable
202HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
203 (longjmp and C++ exceptions *are* supported)
204SUMMARY: AddressSanitizer: stack-buffer-overflow (/home/sebastiano/a.out+0x401330) (BuildId: e8411facf942864956c817b9be7bee9868ec6065) in main
205Shadow bytes around the buggy address:
206 0x7fc4e24ffd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
207 0x7fc4e24ffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
208 0x7fc4e24ffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
209 0x7fc4e24fff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
210 0x7fc4e24fff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
211=>0x7fc4e2500000: f1 f1 f1 f1 00 00[f3]f3 00 00 00 00 00 00 00 00
212 0x7fc4e2500080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
213 0x7fc4e2500100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
214 0x7fc4e2500180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
215 0x7fc4e2500200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
216 0x7fc4e2500280: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
217Shadow byte legend (one shadow byte represents 8 application bytes):
218 Addressable: 00
219 Partially addressable: 01 02 03 04 05 06 07
220 Heap left redzone: fa
221 Freed heap region: fd
222 Stack left redzone: f1
223 Stack mid redzone: f2
224 Stack right redzone: f3
225 Stack after return: f5
226 Stack use after scope: f8
227 Global redzone: f9
228 Global init order: f6
229 Poisoned by user: f7
230 Container overflow: fc
231 Array cookie: ac
232 Intra object redzone: bb
233 ASan internal: fe
234 Left alloca redzone: ca
235 Right alloca redzone: cb
236==15152==ABORTIN
237```
238
239A bit overwhelming, but I certainly prefer this over getting random
240garbage results!
241
242Similarly, one can check for undefined behavior (`-fsanitize=undefined`),
243data races (`-fsanitize=thread`), memory leaks (`-fsanitize=leak`) and
244more. If you never used sanitizers, you should start now!
245
246Sanitizers is that they are *runtime* checks. This means that your
247program running fine with sanitizers enabled does not guarantee the
248absence of errors. Moreover, a program compiled with sanitizers enabled
249will run much slower than normal - so you should only use them in your
250debug builds.
251
252It would be amazing to have this kind of checks performed at compile
253time - I wonder if this is this "Rust" thing all the cool kids are
254talking about?
255
256## Data races and atomic types
257
258Before starting this project I knew the basics of multi-threaded
259programming with [pthreads](https://en.wikipedia.org/wiki/Pthreads). But
260I had (and probably still have) many things to learn.
261
262For example, I used to think that having a thread read a value that
263another thread is possibly writing at the same time could be
264done safely, without using any
265[lock](https://en.wikipedia.org/wiki/Lock_(computer_science)).
266
267Assuming you don't care if the value you read comes from before or after
268the write from the other thread, which was my case, this sounds like
269a reasonable assumption. It turns out that this assumption is called
270*sequential consistency*, and that it is almost always wrong.
271
272One way to confirm that a concurrent read + write is undefined
273behavior in C and C++ is compiling with `-fsanitize=thread`, which
274will rightfully scream at you if one such data race happens. Luckily,
275it is not necessary to use a mutex for this kind of operation:
276starting from C11 and C++11 you can just declare your variable
277[`_Atomic`](https://en.cppreference.com/w/c/language/atomic) and happily
278read and modify it at the same time. This comes at a performance cost,
279but this cost is smaller than that of a traditional lock.
280
281## Single instruction, multiple data
282
283[SIMD](https://en.wikipedia.org/wiki/Single_instruction,_multiple_data)
284is a type of parallelism implemented in pretty much every CPU made in
285the last 20 years or so. You can call SIMD instructions directly from
286high-level code using *vector intrinsics*. For example, the following
287code computes four sums of 64-bit integers at the same time:
288
289```
290/* Compile with -mavx2 option */
291
292#include <stdio.h>
293#include <immintrin.h>
294
295int main() {
296 long long r[4];
297
298 /* Initialize the 4x 64-bit int data structures */
299 __m256i a = _mm256_set_epi64x(23, 42, 1234567890123456789, -1000);
300 __m256i b = _mm256_set_epi64x(-1, 69, -1234567890123456789, 3);
301
302 /* Compute the sums */
303 __m256i c = _mm256_add_epi64(a, b);
304
305 /* Store back the result into a buffer */
306 _mm256_storeu_si256((__m256i *)r, c);
307
308 /* Print the results (last is first) */
309 printf("Results:\n%lld\n%lld\n%lld\n%lld\n", r[0], r[1], r[2], r[3]);
310}
311
312```
313
314The `__mm256_add_epi64()` used above is not just syntactic
315sugar: the compiler translates it directly to a single
316`vpaddq` assembly instruction. You can check this on
317[godbolt](https://godbolt.org/z/d6TjPzKPG), or by compiling the code
318above with `gcc -S -mavx2 file.c`.
319
320Adding numbers is not the only thing that can be done with SIMD: there
321are also instructions for other operations, including logic operators
322and permutations of blocks of bits. The latter was particularly relevant
323to me, this project being a Rubik's cube solver.
324
325Using these instructions gave me a similar feeling to when I wrote
326multi-threaded code for the first time: it looks scary at first
327because of the weird syntax, but it was actually pretty easy to do
328what I wanted once I got started.
329
330## Testing
331
332It sounds completely trivial now, but I did not do much testing
333in my previous personal projects. But rewriting from scratch allowed
334me to do the right thing from the start and implement a proper
335testing suite.
336
337I used different types of tests at different stage of the development.
338In ealy stages, when I was building the core routines of the library,
339I relied more on [unit tests](https://en.wikipedia.org/wiki/Unit_testing),
340and even did some
341[test-driven development](https://en.wikipedia.org/wiki/Test-driven_development).
342These tests ran always in debug mode with *sanitizers* switched on (see
343above) and enabled me to catch errors early and be confident that, once
344the tests passed, the code was correct. On later stages this strategy
345was hard to apply, because the high-level functions required working
346with large arrays of data that were multiple gigabytes in size; they
347could not be tested in isolation.
348
349Being completely nuts as I am, I set up the whole testing system
350from the ground up, but this turned out to be extremely practical and also
351quite powerful in its simplicity. In a folder called `test` I had a
352`test.sh` file and one sub-folder for each test. Each test consists
353of a (usually very short) `.c` file and one or more *test cases*,
354i.e. a pair of `file.in` (input file) and `file.out` (expected output)
355files. The `test.sh` script, which I call with `make test`, builds and
356runs each test case and compares the result to the expected output using
357diff(1). Optionally, one case use an environment variable to pick which
358test(s) to run.
359
360If you are wondering why I set this whole thing up and I did not use
361some test framework, that's a fair question. The answer is that I like to
362understand every part of something I build, and I have quite a distaste
363for external dependencies. Moreover, I was not in a rush to complete
364this project - I was happy to build it piece by piece and possibly go
365back and change everything if I discovered a better way to do things.
366
367By doing everything by hand I ended up re-discovering many well-known
368tricks on my own, such as [callback functions](../2024-06-20-callback-log)
369and [some macro magic](../2023-11-14-test-visibility-c-macro).
370
371But in the end it does not matter that much what type of tests or what
372framework one uses, as long as they are automated and easy to run.
373
374## Structuring the project as a library
375
376This one is a rare instance of learning something not by banging my head
377against a wall until either cracks, but instead by making a good call
378intuitively early on and confirming it empirically.
379
380When I started the rewrite I had in the the back of my mind the idea
381of building multiple UIs for this tool, possibly using different
382languages and frameworks. So I decided to structure the core of the project
383as a library *with a very thin interface*. This meant not exposing many
384functions, types or constants to simplify writing adapter code for
385other languages. Indeed I avoided exposing *any* type at all and only
386used standard integer types and strings. This made it easy to implement
387Python bindings (see below) and at the same time it gave me more freedom
388to reorganize the code internally without breaking the API.
389
390Another thing that I did was deferring all I/O and memory management
391to the *user* of the library. This implied for example using
392[callback functions](../2024-06-20-callback-log) for logging and,
393as discussed at the beginning of this post, returning string output via a
394`char *` buffer provided by the caller. This forced me to write in a very
395clean and safe style - there is only one `malloc()` in the core library!
396
397## Interfacing with Python
398
399This project was developed as a library, but I still needed to run the
400code to test it, of course. To do this, at first I developed a simple
401command line tool to call this library's functions directly. However
402parsing command line options by hand can easily become messy.
403
404At some point I thought it would be cool to write some Python adapters
405for my library and use the Python REPL instead of my custom CLI. Not only
406would this be more powerful because of the ability to run arbitrary code,
407it would also be easier to write and maintain. It was something new for
408me, but once again it turned to be not too hard. I wrote about the first
409steps in [a blog post](..2024-10-08-python-c).
410
411## Accepting contributors
412
413Around April last year a friend asked me if he could work on this
414project as part of his bachelor thesis in computer science. And I said
415no, because I wanted this to be my personal, slow-paced project and I
416could not assure him that it would be in a stable state while he was
417working on his thesis. So I made
418[a spin-off project](https://git.tronto.net/cubecore)
419and told him he could base his work on that. I offered to help if he
420had any questions on the topice, and that was it.
421
422Bur when he asked again to contribute a few months later, the situation
423had changed. The project was in a stable state and most of the preliminary
424work was done, so I decided to let him have fun and implement one of the
425core parts of the tool. I started reviewing his contributions, and I had
426to find the right balance between keeping the code consistent with what
427I had mind and let him submit his code without requesting unnecessary
428changes. This is something I regularly do for work, but doing this for
429a project of which I am the creator and main author felt much different!
430
431Besides the learning experience, this also had some technical benefits,
432mostly related to the fact that we are using different compilers,
433operating system and CPU architectures: now I have a library that compiles
434with both GCC and Clang, runs on Linux and MacOS, and is optimized for
435both x86 with
436[AVX2 extensions](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#Advanced_Vector_Extensions_2)
437and ARM with
438[NEON](https://en.wikipedia.org/wiki/ARM_architecture_family#Advanced_SIMD_(Neon)).
439
440## Conclusion
441
442Rewriting from scratch is rarely the fastest way to fix a project,
443but it feels nice and, as this post shows, it is a great opportunity to
444learn cool new stuff.

Generated with cgit - Back to sebastiano.tronto.net