3 ms·
> It’s more difficult to get right than one big single source file. For all my personal projects, I use a single main.c file which #includes the topologically
by psykotic 6y ago
> It’s more difficult to get right than one big single source file.
For all my personal projects, I use a single main.c file which #includes the topologically sorted .c files for each module, one file per module, preceded by a shared #include block for external library headers. If your module dependency graph is acyclic, you don't need any per-module headers; inside a module everything is sorted and you only need forward declarations for mutually recursive functions/types. The only real downside for me is that it breaks 'static' isolation. Even the slower C compilers like gcc, clang and msvc can build 50-100 kloc/sec on my aging laptop with code structured like this.
You get qualify of life benefits like fast, simple builds (one-liner scripts for building on each platform, no need for the compiler to chew through the same monstrous system headers for every .c file, fewer symbols for the linker to resolve) and less boilerplate you have to write (no redundant copies of declarations in headers, no redundant #include blocks at the top of every .c file). In case it's something you care about, structuring your code like this also makes it trivial to automatically amalgamate your entire project into an stb-style single-file header library for distribution purposes: it's already 'static' safe, you've already specified the topological order and you've already eliminated all the redundant boilerplate you wouldn't want in a single-file distribution, so it's mostly just a matter of writing a script that inlines the #include "foo.c" directives in the main.c file.
- suyjuris 6y agoI do the same thing, it works great!
- rightbyte 6y agoThat is really neat. Never thought of just including the c-files in a c-file. There is a breakpoint between invoking gcc manually and having a build system like make or cmake that is quite annoying and this would be a nice bridge.
- rramadass 6y agoNeat technique. I had used it in a project where there was no linker and the compiler produced an absolute addressed binary for a custom chip. The flip side of this technique is to be careful about blowing up the size of the final executable since everything gets included. You may need a linker pass to remove unused sections from the final executable. One good example to manage this is found in the dinkumware C library where each function is in its own .c (and hence corresponding .o) file. This way only the functions actually used get linked into the final executable.
- psykotic 6y ago> The flip side of this technique is to be careful about blowing up the size of the final executable since everything gets included. Yeah, the 1970s linker model is very silly like this. The good news is that link-time optimization will handle this for you nowadays without any special per-symbol mark-up. As an experiment, put an unused 'deadfunc' function definition in a file called ltotest.c and then compare gcc -O2 ltotest.c -o ltotest && (objdump -D ltotest | grep deadfunc) and gcc -O2 -flto ltotest.c -o ltotest && (objdump -D ltotest | grep deadfunc)
- rramadass 6y agohttps://tetzank.github.io/posts/removing-unused-code/ https://tetzank.github.io/posts/removing-unused-code/
- psykotic 6y agoNeat, I didn't know about -fwhole-program as an alternative to -flto for single-file builds. It should help with compile times a little bit (though I normally only use LTO for release builds, rarely during development, so it's not a big deal either way). With MSVC I think you still have to use LTO (or LTCG as it's called there) to get this effect.