diff options
| author | Sebastiano Tronto <sebastiano@tronto.net> | 2025-04-05 11:06:21 +0200 |
|---|---|---|
| committer | Sebastiano Tronto <sebastiano@tronto.net> | 2025-04-05 11:06:21 +0200 |
| commit | 9ee63bff4c8c631e1cb69944ed49e64a9f91f94c (patch) | |
| tree | 39a53127aee617678daaa2d6e140354f05b3a16e | |
| parent | 7219836b03f169f9ea69e4dfdf3f35dfdba80ab0 (diff) | |
| download | sebastiano.tronto.net-9ee63bff4c8c631e1cb69944ed49e64a9f91f94c.tar.gz sebastiano.tronto.net-9ee63bff4c8c631e1cb69944ed49e64a9f91f94c.zip | |
Blog post
| -rw-r--r-- | src/blog/2025-04-05-learnt-rewrite/learned-rewrite.md | 444 |
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 | |||
| 3 | About two years ago I [wrote](../2023-04-10-the-big-rewrite) about a | ||
| 4 | project I have been working on and off for a few years. In that post | ||
| 5 | I explain how and why I decided to rewrite it from scracth, splitting | ||
| 6 | it in multiple parts. I ended up not following exactly the plan that I | ||
| 7 | laid out, and I also worked on the rewrite more slowly than I wanted. | ||
| 8 | But rewriting from scratch allowed me to experiment with new ideas and | ||
| 9 | discover new tricks, and after two years I learned enough stuff that I | ||
| 10 | can make a blog post about it! | ||
| 11 | |||
| 12 | The project I am talking about is [Nissy](https://nissy.tronto.net), a | ||
| 13 | command-line Rubik's Cube solver that is quite popular in the very small | ||
| 14 | niche of speedcubers interested in the *Fewest Moves Challenge*, which | ||
| 15 | consists in solving the puzzle with the smallest amount of moves rather | ||
| 16 | than as fast as possible. Both the old and the new versions are written | ||
| 17 | in C, but not all the things I discuss in this post are language-specific. | ||
| 18 | |||
| 19 | You can find the new version of this project, the "Big Rewrite", | ||
| 20 | in [this repository](https://git.tronto.net/h48) - or | ||
| 21 | [on github](https://github.com/sebastianotronto/h48), if you prefer. | ||
| 22 | I decided to rename it "H48" back when I planned to have separate projects | ||
| 23 | for the different features of the old version, but I may change it back | ||
| 24 | to Nissy at some point. | ||
| 25 | |||
| 26 | But now let's get into it! | ||
| 27 | |||
| 28 | ## Better memory safety (yes, even in C) | ||
| 29 | |||
| 30 | Let's start with a mistake that I think many inexperienced C programmers | ||
| 31 | make: 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 | ||
| 33 | memory I used, in most cases I would have been better off not using | ||
| 34 | dynamic memory allocation at all. | ||
| 35 | |||
| 36 | As an example, let's consider the case of converting some data structure | ||
| 37 | into a string. One way to do it would be something like this: | ||
| 38 | |||
| 39 | ``` | ||
| 40 | char *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 | |||
| 50 | If you are familiar with | ||
| 51 | [garbage-collected languages](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)), | ||
| 52 | you may find the code above nice and clean. However, since with C we have | ||
| 53 | to manage our memory manually, the caller of this function would have | ||
| 54 | to remember to `free()` the pointer returned by `my_type_to_string()`. | ||
| 55 | This is not rocket science, but it is a common source of mistakes. | ||
| 56 | |||
| 57 | However, a safer pattern for this kind of function would be the following: | ||
| 58 | |||
| 59 | ``` | ||
| 60 | int 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 | |||
| 72 | In this new version of the code, instead of returning a string, the | ||
| 73 | function takes a `char` buffer and its lenght as a parameter. Notice | ||
| 74 | that 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 | ||
| 77 | square-bracket notation the compiler may be able to catch a too-small | ||
| 78 | buffer and warn us about it. | ||
| 79 | |||
| 80 | Now we can get rid of the malloc entirely, because the caller can | ||
| 81 | just allocate the char buffer on its stack: | ||
| 82 | |||
| 83 | ``` | ||
| 84 | void 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 | |||
| 95 | Unfortuntely, this new pattern leads to a small problem: the caller | ||
| 96 | must 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, | ||
| 98 | including handling a `NULL` first parameter as a "dry-run" request that | ||
| 99 | returns the required size without writing anything, or using a global | ||
| 100 | constant. | ||
| 101 | |||
| 102 | This brings me to another feature I have started using more and more | ||
| 103 | recently. Say we want to use a global constant for the size of `mytype_t`. | ||
| 104 | We could do something like this: | ||
| 105 | |||
| 106 | ``` | ||
| 107 | #define MYTYPE_SIZE 512 | ||
| 108 | |||
| 109 | int 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 | |||
| 115 | void doing_stuff(mytype_t x) { | ||
| 116 | char str[MYTYPE_SIZE]; | ||
| 117 | mytype_to_string(&x, str); | ||
| 118 | |||
| 119 | /* ... */ | ||
| 120 | } | ||
| 121 | ``` | ||
| 122 | |||
| 123 | Notice how I used `[static MYTYPE_SIZE]` to denote the required size | ||
| 124 | of the buffer parameter. This tells the compiler that the function | ||
| 125 | `mytype_to_string()` can assume that the buffer must reference an area | ||
| 126 | of memory of size at least `MYTYPE_SIZE`. In turn, the compiler can | ||
| 127 | use this information not only to optimize the code, but also to give | ||
| 128 | useful warnings. This trick can also be used to tell the compiler that | ||
| 129 | a pointer cannot be `NULL`: a pointer is not `NULL` if it refers to an | ||
| 130 | valid area of memory of size at least 1. So we can declare a function | ||
| 131 | as follows: | ||
| 132 | |||
| 133 | ``` | ||
| 134 | int mytype_to_string(const mtytype_t x[static 1], char buffer[static MYTYPE_SIZE]) { | ||
| 135 | /* ... */ | ||
| 136 | } | ||
| 137 | ``` | ||
| 138 | |||
| 139 | With this we can come up with a set of rules to never de-reference a | ||
| 140 | NULL pointer. A pointer must be either: | ||
| 141 | |||
| 142 | 1. Obtained by de-referencing an object allocated in current function. | ||
| 143 | 2. A parameter declared as `[static 1]`. | ||
| 144 | 3. Checked for `NULL` before de-referencing. | ||
| 145 | |||
| 146 | Having more memory safety baked into the language would be more convenient, | ||
| 147 | but at least we can use some mitigations. | ||
| 148 | |||
| 149 | ## Sanitizers | ||
| 150 | |||
| 151 | Another tool that help me improve the memory management of my code are | ||
| 152 | [sanitizers](https://en.wikipedia.org/wiki/Code_sanitizer). | ||
| 153 | They were suggested to me by [Tomas Rokicki](https://tomas.rokicki.com) | ||
| 154 | when I shared my struggles with | ||
| 155 | [an old bug](../2023-05-05-debug-smartphone), and now that I got used | ||
| 156 | to them I am wondering how I even survived programming in C without them. | ||
| 157 | Sanitizers are *compiler intrumentations* supported by the major C and | ||
| 158 | C++ compilers - namely GCC and Clang. They add some runtime checks to | ||
| 159 | a program that make it crash with a clear message if they detect | ||
| 160 | a memory error, such as an access outside the bounds of an array, or a | ||
| 161 | race condition. For example, say you try to access some out-of-bound | ||
| 162 | element of an array: | ||
| 163 | |||
| 164 | ``` | ||
| 165 | /* This is a.c */ | ||
| 166 | |||
| 167 | #include <stdio.h> | ||
| 168 | |||
| 169 | int main() { | ||
| 170 | int a[4] = {0, 1, 2, 3}; | ||
| 171 | |||
| 172 | printf("Out-of-bound value: %d\n", a[4]); | ||
| 173 | } | ||
| 174 | ``` | ||
| 175 | |||
| 176 | Normally, this program just runs fine and prints some garbage value: | ||
| 177 | |||
| 178 | ``` | ||
| 179 | $ cc a.c && ./a.out | ||
| 180 | Out-of-bound value: 1573265008 | ||
| 181 | ``` | ||
| 182 | |||
| 183 | Notice how the compiler did not even give us a warning! | ||
| 184 | However, 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 | ||
| 191 | READ 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 | |||
| 197 | Address 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 | ||
| 202 | HINT: 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) | ||
| 204 | SUMMARY: AddressSanitizer: stack-buffer-overflow (/home/sebastiano/a.out+0x401330) (BuildId: e8411facf942864956c817b9be7bee9868ec6065) in main | ||
| 205 | Shadow 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 | ||
| 217 | Shadow 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 | |||
| 239 | A bit overwhelming, but I certainly prefer this over getting random | ||
| 240 | garbage results! | ||
| 241 | |||
| 242 | Similarly, one can check for undefined behavior (`-fsanitize=undefined`), | ||
| 243 | data races (`-fsanitize=thread`), memory leaks (`-fsanitize=leak`) and | ||
| 244 | more. If you never used sanitizers, you should start now! | ||
| 245 | |||
| 246 | Sanitizers is that they are *runtime* checks. This means that your | ||
| 247 | program running fine with sanitizers enabled does not guarantee the | ||
| 248 | absence of errors. Moreover, a program compiled with sanitizers enabled | ||
| 249 | will run much slower than normal - so you should only use them in your | ||
| 250 | debug builds. | ||
| 251 | |||
| 252 | It would be amazing to have this kind of checks performed at compile | ||
| 253 | time - I wonder if this is this "Rust" thing all the cool kids are | ||
| 254 | talking about? | ||
| 255 | |||
| 256 | ## Data races and atomic types | ||
| 257 | |||
| 258 | Before starting this project I knew the basics of multi-threaded | ||
| 259 | programming with [pthreads](https://en.wikipedia.org/wiki/Pthreads). But | ||
| 260 | I had (and probably still have) many things to learn. | ||
| 261 | |||
| 262 | For example, I used to think that having a thread read a value that | ||
| 263 | another thread is possibly writing at the same time could be | ||
| 264 | done safely, without using any | ||
| 265 | [lock](https://en.wikipedia.org/wiki/Lock_(computer_science)). | ||
| 266 | |||
| 267 | Assuming you don't care if the value you read comes from before or after | ||
| 268 | the write from the other thread, which was my case, this sounds like | ||
| 269 | a reasonable assumption. It turns out that this assumption is called | ||
| 270 | *sequential consistency*, and that it is almost always wrong. | ||
| 271 | |||
| 272 | One way to confirm that a concurrent read + write is undefined | ||
| 273 | behavior in C and C++ is compiling with `-fsanitize=thread`, which | ||
| 274 | will rightfully scream at you if one such data race happens. Luckily, | ||
| 275 | it is not necessary to use a mutex for this kind of operation: | ||
| 276 | starting from C11 and C++11 you can just declare your variable | ||
| 277 | [`_Atomic`](https://en.cppreference.com/w/c/language/atomic) and happily | ||
| 278 | read and modify it at the same time. This comes at a performance cost, | ||
| 279 | but 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) | ||
| 284 | is a type of parallelism implemented in pretty much every CPU made in | ||
| 285 | the last 20 years or so. You can call SIMD instructions directly from | ||
| 286 | high-level code using *vector intrinsics*. For example, the following | ||
| 287 | code 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 | |||
| 295 | int 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 | |||
| 314 | The `__mm256_add_epi64()` used above is not just syntactic | ||
| 315 | sugar: 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 | ||
| 318 | above with `gcc -S -mavx2 file.c`. | ||
| 319 | |||
| 320 | Adding numbers is not the only thing that can be done with SIMD: there | ||
| 321 | are also instructions for other operations, including logic operators | ||
| 322 | and permutations of blocks of bits. The latter was particularly relevant | ||
| 323 | to me, this project being a Rubik's cube solver. | ||
| 324 | |||
| 325 | Using these instructions gave me a similar feeling to when I wrote | ||
| 326 | multi-threaded code for the first time: it looks scary at first | ||
| 327 | because of the weird syntax, but it was actually pretty easy to do | ||
| 328 | what I wanted once I got started. | ||
| 329 | |||
| 330 | ## Testing | ||
| 331 | |||
| 332 | It sounds completely trivial now, but I did not do much testing | ||
| 333 | in my previous personal projects. But rewriting from scratch allowed | ||
| 334 | me to do the right thing from the start and implement a proper | ||
| 335 | testing suite. | ||
| 336 | |||
| 337 | I used different types of tests at different stage of the development. | ||
| 338 | In ealy stages, when I was building the core routines of the library, | ||
| 339 | I relied more on [unit tests](https://en.wikipedia.org/wiki/Unit_testing), | ||
| 340 | and even did some | ||
| 341 | [test-driven development](https://en.wikipedia.org/wiki/Test-driven_development). | ||
| 342 | These tests ran always in debug mode with *sanitizers* switched on (see | ||
| 343 | above) and enabled me to catch errors early and be confident that, once | ||
| 344 | the tests passed, the code was correct. On later stages this strategy | ||
| 345 | was hard to apply, because the high-level functions required working | ||
| 346 | with large arrays of data that were multiple gigabytes in size; they | ||
| 347 | could not be tested in isolation. | ||
| 348 | |||
| 349 | Being completely nuts as I am, I set up the whole testing system | ||
| 350 | from the ground up, but this turned out to be extremely practical and also | ||
| 351 | quite 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 | ||
| 353 | of a (usually very short) `.c` file and one or more *test cases*, | ||
| 354 | i.e. a pair of `file.in` (input file) and `file.out` (expected output) | ||
| 355 | files. The `test.sh` script, which I call with `make test`, builds and | ||
| 356 | runs each test case and compares the result to the expected output using | ||
| 357 | diff(1). Optionally, one case use an environment variable to pick which | ||
| 358 | test(s) to run. | ||
| 359 | |||
| 360 | If you are wondering why I set this whole thing up and I did not use | ||
| 361 | some test framework, that's a fair question. The answer is that I like to | ||
| 362 | understand every part of something I build, and I have quite a distaste | ||
| 363 | for external dependencies. Moreover, I was not in a rush to complete | ||
| 364 | this project - I was happy to build it piece by piece and possibly go | ||
| 365 | back and change everything if I discovered a better way to do things. | ||
| 366 | |||
| 367 | By doing everything by hand I ended up re-discovering many well-known | ||
| 368 | tricks on my own, such as [callback functions](../2024-06-20-callback-log) | ||
| 369 | and [some macro magic](../2023-11-14-test-visibility-c-macro). | ||
| 370 | |||
| 371 | But in the end it does not matter that much what type of tests or what | ||
| 372 | framework one uses, as long as they are automated and easy to run. | ||
| 373 | |||
| 374 | ## Structuring the project as a library | ||
| 375 | |||
| 376 | This one is a rare instance of learning something not by banging my head | ||
| 377 | against a wall until either cracks, but instead by making a good call | ||
| 378 | intuitively early on and confirming it empirically. | ||
| 379 | |||
| 380 | When I started the rewrite I had in the the back of my mind the idea | ||
| 381 | of building multiple UIs for this tool, possibly using different | ||
| 382 | languages and frameworks. So I decided to structure the core of the project | ||
| 383 | as a library *with a very thin interface*. This meant not exposing many | ||
| 384 | functions, types or constants to simplify writing adapter code for | ||
| 385 | other languages. Indeed I avoided exposing *any* type at all and only | ||
| 386 | used standard integer types and strings. This made it easy to implement | ||
| 387 | Python bindings (see below) and at the same time it gave me more freedom | ||
| 388 | to reorganize the code internally without breaking the API. | ||
| 389 | |||
| 390 | Another thing that I did was deferring all I/O and memory management | ||
| 391 | to the *user* of the library. This implied for example using | ||
| 392 | [callback functions](../2024-06-20-callback-log) for logging and, | ||
| 393 | as 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 | ||
| 395 | clean and safe style - there is only one `malloc()` in the core library! | ||
| 396 | |||
| 397 | ## Interfacing with Python | ||
| 398 | |||
| 399 | This project was developed as a library, but I still needed to run the | ||
| 400 | code to test it, of course. To do this, at first I developed a simple | ||
| 401 | command line tool to call this library's functions directly. However | ||
| 402 | parsing command line options by hand can easily become messy. | ||
| 403 | |||
| 404 | At some point I thought it would be cool to write some Python adapters | ||
| 405 | for my library and use the Python REPL instead of my custom CLI. Not only | ||
| 406 | would this be more powerful because of the ability to run arbitrary code, | ||
| 407 | it would also be easier to write and maintain. It was something new for | ||
| 408 | me, but once again it turned to be not too hard. I wrote about the first | ||
| 409 | steps in [a blog post](..2024-10-08-python-c). | ||
| 410 | |||
| 411 | ## Accepting contributors | ||
| 412 | |||
| 413 | Around April last year a friend asked me if he could work on this | ||
| 414 | project as part of his bachelor thesis in computer science. And I said | ||
| 415 | no, because I wanted this to be my personal, slow-paced project and I | ||
| 416 | could not assure him that it would be in a stable state while he was | ||
| 417 | working on his thesis. So I made | ||
| 418 | [a spin-off project](https://git.tronto.net/cubecore) | ||
| 419 | and told him he could base his work on that. I offered to help if he | ||
| 420 | had any questions on the topice, and that was it. | ||
| 421 | |||
| 422 | Bur when he asked again to contribute a few months later, the situation | ||
| 423 | had changed. The project was in a stable state and most of the preliminary | ||
| 424 | work was done, so I decided to let him have fun and implement one of the | ||
| 425 | core parts of the tool. I started reviewing his contributions, and I had | ||
| 426 | to find the right balance between keeping the code consistent with what | ||
| 427 | I had mind and let him submit his code without requesting unnecessary | ||
| 428 | changes. This is something I regularly do for work, but doing this for | ||
| 429 | a project of which I am the creator and main author felt much different! | ||
| 430 | |||
| 431 | Besides the learning experience, this also had some technical benefits, | ||
| 432 | mostly related to the fact that we are using different compilers, | ||
| 433 | operating system and CPU architectures: now I have a library that compiles | ||
| 434 | with both GCC and Clang, runs on Linux and MacOS, and is optimized for | ||
| 435 | both x86 with | ||
| 436 | [AVX2 extensions](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#Advanced_Vector_Extensions_2) | ||
| 437 | and ARM with | ||
| 438 | [NEON](https://en.wikipedia.org/wiki/ARM_architecture_family#Advanced_SIMD_(Neon)). | ||
| 439 | |||
| 440 | ## Conclusion | ||
| 441 | |||
| 442 | Rewriting from scratch is rarely the fastest way to fix a project, | ||
| 443 | but it feels nice and, as this post shows, it is a great opportunity to | ||
| 444 | learn cool new stuff. | ||
