10 ms·
How to Structure C Projects: These Best Practices Worked for Me
- danjl 3y agoIsn't this basically, "how to structure a large project". The bits that are C-specific are small. This could easily be changed to JavaScript, Rust, Java or any language with only small modifications.
- thesnide 3y agoI sense incoming bikeshedding in the comments. Just have a look at how many C make replacement exists
- Joker_vD 3y ago> The downside of this approach is that you will need to keep the Makefile updated with the new files you add to the project so that they are compiled and linked correctly. Does it? IIRC something like build/%.o: src/%.c include/*.h $(CC) -c $(CFLAGS) $(CPPFLAGS) -c -o $@ $< works well enough for small projects.
- seabass-labrax 3y agoYou don't even need to specify that rule with GNU Make: building %.o is a built-in rule[1]. Personally, I tend to prefer specifying the files individually - it doesn't take up that much developer time nor much space in your Makefile, and makes it considerably easier to debug building issues when using a sandboxed environment (such as Nix or Bazel). [1]: https://www.gnu.org/software/make/manual/html_node/Catalogue-of-Rules.html#index-C_002c-rule-to-compile https://www.gnu.org/software/make/manual/html_node/Catalogue...
- Joker_vD 3y agoDoesn't the built-in rule put files in the wrong folder? That is, the same folder the source file is in?
- seabass-labrax 3y agoYes; I've just tested it on GNU Make 4.3 and you are quite right. It only works when the *.c files are in the directory where object files are referenced. E.g.: example: build/blah.o echo nothing special .PHONY: example ...requires blah.c to be in build/.
- cozzyd 3y agoYou can probably use VPATH as a hack
- lsllc 3y agoI use make's `wildcard`, for example to locate all of my unit tests with the project sources (I don't use a `test` subdir): TEST_SRC = $(wildcard *_test.c) If you follow TFA's guidelines about having a `src` dir, then: SRC = $(wildcard src/*.c) And if you need to convert that to a list of matching .o files, `patsubst` is your friend: OBJ = $(patsubst %.c,%.o,$(SRC)) So now simply dropping a file into the right place (or a unit test ending in _test.c) won't require any makefile changes. I tend to copy my most recent Makefile into my next project and generally it stays 90% the same (I have to edit a few macro definitions and rules, but not many).
- deleted 3y ago[deleted]
- aninteger 3y agoI've always preferred Stephen Brennan's approach mentioned here: https://brennan.io/2020/05/08/meson/ https://brennan.io/2020/05/08/meson/ This is the approach I take with my C and C++ projects, although I've swapped meson out for CMake. I still have my CMake projects generate pkg-config files as well so that I don't force CMake on any consuming projects (just because I suffered with CMake doesn't mean you have to, although you can as well :))
- emmelaich 3y agoFWIW practices is misspelled "pratices" twice. Just remove best-practices, it's a silly phrase.
- veltas 3y agoKeeping .c and .h files separate is pointless, *.c and *.h can pick them out when necessary. 'include' is a good choice for libraries, the external interface, but not necessary for internal headers.
- actionfromafar 3y agoIf you have a bunch of platform specific code it can be quite neat to have .c and .h files separately. Then each platform gets its own directory with .c files.
- masfoobar 3y agoWhen I build C projects which, admittedly, is making a game, I always start off with platform specific code. This is very handy by splitting up .h and .c files. ie... platform.h platform_windows.c platform_linux.c etc. This way, my build file for windows uses platform_windows.c, etcetc.
- kerkeslager 3y agoThis (among other reasons) is why I'm moving a few of my C projects over to Rust. Whenever I'm spending a half hour debugging why my build system isn't picking up some file or some flag is getting passed wrong, I end up asking myself, "Why am I doing this?" My entire build system in Rust projects consists of the commands "cargo build", "cargo run", and "cargo test" and I have yet to find a reason to deviate from the default filesystem structure or configurations. To be clear there are real reasons to still be writing C. No hate to the author of this post. I just don't want to be doing it any more in most cases.
- colonwqbang 3y agoYour problems are not with C the language but with the chosen build tools. Have you tried using a more modern tool like Meson? It is more comparable to Cargo. To start a new project today using plain GNU Make like the author of the article recommends, is not a good idea unless you enjoy fiddling with the build.
- flykespice 3y ago> Have you tried using a more modern tool like Meson? It is more comparable to Cargo. Not comparable at all. The difference is Cargo is the official standard package manager of the Rust language. Meson is one among the sea of incompatible build systems for the C language.
- binary132 3y agoThat's why we need a new build system / package manager for C. /s (but not really)
- funnyflywheel 3y agohttps://xkcd.com/927 https://xkcd.com/927 comes to mind.
- 3y ago
- fforflo 3y agoI'm not so sure about having bin/lib in the project dir. I've found it more helpful to have a PREFIX=$(pwd)/build or even PREFIX=$HOME/.local as an installation destination. And have a .env add PATH=$PREFIX/bin:PATH
- fforflo 3y agoI'm not so sure with having bin/lib in the project dir. I've found it more helpful to have a PREFIX=$(pwd)/build or even PREFIX=$HOME/.local as an installation destination. And have a .env add PATH=$PREFIX/bin:PATH
- Arch-TK 3y agoThe recommended Makefile has several issues, not least of which being that it relies on dependency fulfilment order which can be non-deterministic (e.g. if you pass -j). The project structure includes flavour-of-the-week litter like .devcontainer, .github, .vscode. compile_commands.json should be getting generated, not stored. Not sure if I would recommend anyone follow this information. The build system situation in the C world is dire. And I should know, given I write my own build systems for C. I would say, while I've never actually used it, Meson seems the least non-sensical out of the box experience. Ideal in some ways but not in others is designing and implementing your own build system because genuinely everything else out there has some major shortcoming or other. If you are insistent on make, stick to GNU make, read the GNU make manual, understand make and then write something simple and non-recursive. This is the most recent Makefile I helped create: CXX ?= clang++ CXXFLAGS += -std=c++20 -Wall -Wextra -fsanitize=address -ggdb3 -MMD -MP LDFLAGS += -fsanitize=address OBJ := program.o parse.o all: program compile_flags.txt program: $(OBJ) $(CXX) $(LDFLAGS) $(TARGET_ARCH) $^ $(LDLIBS) -o $@ $(OBJ): Makefile compile_flags.txt: Makefile printf "%s\n" $(CXXFLAGS) >$@ clean: $(RM) *.o *.d program compile_flags.txt -include $(OBJ:.o=.d) .PHONY: clean If you want to do something like run a linter, code formatter, etc, before building, then this should be performed by a script which calls into make. Make is not designed to be used as a switch statement for your bash snippets.
- ReleaseCandidat 3y ago> The build system situation in the C world is dire. Meta's Buck 2 (written is Rust) would be my choice, if I would write "new" C or C++. Examples using Vcpkg https://github.com/Release-Candidate/Cxx-Buck2-vcpkg-Examples https://github.com/Release-Candidate/Cxx-Buck2-vcpkg-Example... And Conan https://github.com/Release-Candidate/Cxx-Buck2-Conan-Examples https://github.com/Release-Candidate/Cxx-Buck2-Conan-Example...
- humanrebar 3y agoIf you're writing C, you might not want tools written in higher level languages required for your build environment. This is why you still see a lot of important C project support autotools even though it's a pretty awful developer experience.
- ReleaseCandidat 3y agoAm I missing something, or are the examples both projects with less than 20 C source files?
- JonChesterfield 3y agoI write a lot of C. I'm somewhat prone to writing things in C that shouldn't be. There are two somewhat unusual things that make that more viable than it sounds. One, write unit tests in the same source as the implementation. Tests that run through the interface are good too and can be in separate source files, maybe in another directory. Fine grain poke at the internals of static functions really should be written as well and those are best placed where they can call said static functions directly. Make adding tests really low friction and write lots of them. Two, embrace code generators. I've seen it written that C++ solves problems by extending the language and C solves problems by writing more C and that sounds about right. Make adding a code generator as low friction as adding a C source file. It should take seconds to convert an existing source file into a generator that recreates said existing source file. People love ceremony. All the source files are written in lists in cmake files. All their build dependencies are written down in the cmake files as well. All the tests are written in list of tests to run. Every code generator is introduced as an independent project with it's own documentation and careful integration into the build systems. I love writing C because I do none of that overhead. The files to compile are whatever is on disk. The tests to run are all the tests in the source, they aren't listed somewhere else as well. Any file called *.lua.c is a lua program that writes the intended contents of *.c to stdout. *.c.h will be compiled as C and then run to write a C header on stdout. Convention over configuration with a language that requires zero cognitive overhead to write.
- memoryperson10 3y agoWild, I was just thinking about how I wanted to do reflection on C structs using LuaJIT FFI for serialization.
- JonChesterfield 3y agoIt's worth considering the other direction - write the struct definition in json or xml, or dicts in python source or whatever, and generate the C structs and the associated functions and data from that. Go slightly further and write out function types like that and you get something equivalent to swig, where instead of parsing C and trying to emit wrappers for other languages from that framework, you have the data in native lua tables that you emit code from.
- whiterknight 3y agoStart by actually writing code. Use gcc command to compile. Put the command in a shell script or make file when you start to get more than a few arguments. Done.
- LabMechanic 3y agoUse MinUnit.h https://jera.com/techinfo/jtns/jtn002 https://jera.com/techinfo/jtns/jtn002 Consider using Clang-Tidy with `-*,cert-*` (Which disables all default options, and checks your code according to the Secure coding standard (CERT) ruleset.) Use Clang-Format as well. If you use Clang, consider `-Weverything -fsanitize=address,undefined` and disable options that you do not need. Consider using NMake or Make (or just write a simple script to automate building the stuff). Here's a Google-esque style guide for C: https://gist.github.com/davidzchen/9187878 https://gist.github.com/davidzchen/9187878 At least, this is my approach.
- LabMechanic 3y agoHere's a simple NMAKE Makefile using MSVC to compile a Win32 application on Windows 11: CC = cl.exe LD = link.exe CFLAGS = /Od /Zi /FAsu /std:c17 /permissive- /W4 /WX LDFLAGS = /DEBUG /MACHINE:X64 /ENTRY:wWinMainCRTStartup /SUBSYSTEM:WINDOWS LDLIBS = user32.lib gdi32.lib PREFIX = ../src/ all: app.exe app.exe: app.obj $(LD) $(LDFLAGS) /OUT:app.exe app.obj $(LDLIBS) app.obj: $(PREFIX)app.c $(CC) /c $(CFLAGS) $(PREFIX)app.c clean: rmdir /q /s build
- deleted 3y ago[deleted]
- hashtag-til 3y agoSounds like the perfect theme for a Cookiecutter template. https://cookiecutter.readthedocs.io/en/stable/ https://cookiecutter.readthedocs.io/en/stable/
- TheCipster 3y agoIn recent times I prefer Xmake[0] to CMake. [0] https://xmake.io https://xmake.io
- Alifatisk 3y agoMy only wish was that C had a package manager, and maybe a dynamic analyzer to could catch bugs that a static analyzer would miss. Also, being able to cross-compile would be cool. But I guess cosmopolitan solves this.
- GuestHNUser 3y agozig cc is a top notch cross compiler that you should check out[0]. [0] https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html https://andrewkelley.me/post/zig-cc-powerful-drop-in-replace...
- Alifatisk 3y agoThat’s so cool, It can compile to many different targets!
- teunispeters 3y agoAh satire. ;) Package manager: dynamic libraries! Or if you want to get really advanced, pkg-config and related. (or static libraries even. Common enough in embedded space!) Dynamic analyzer : valgrind, gdb, Visual Studio, xCode, ... Cross compiling : more platforms supported by C compilers than by any other build tool. Oh sure, you need to eventually watch byte sizes (as 8 bit bytes are a fairly narrow set of platforms), and know something about the platform's kernel API (if any, lots of embedded platforms are just hardware interactions!). So I'll assume this is satire.
- skydhash 3y agoThat makes a while since I wrote C, but I was surprised by GP's comment. I think C has the most tooling available nowadays (they may not be easy to use). And with its interoperability, can be used together with a lot of languages.
- jll29 3y agoNobody is held back from adding further components to the GNU GCC or Clang/LLVM compiler suites. Conan, gdb/DDD, Rational Purify etc. exist and can be replaced/improved by better tools.
- ww520 3y agoJust this week I start looking into Zig. It's a joy to use the language. The compiler and the build tool come in one executable. Most of the needs to decide on the project structure are eliminated. The project scaffold including the directory layout, build files and initial source files is generated with the init command. The build file is written in Zig itself, and have all the directory layout configured. You can modify it but the default structure is pretty sensible. Zig really removes a lot of hurdles in the onboarding process and let you jump into building software right the way.
- jll29 3y agoWhat's proposed as the "modular" approach is close to what I use, perhaps with the following modifications: 1. instead of folders for sub-systems at the top level, put them all into src/. 2. I use the name ext/ instead of lib/ for external libraries, because in POSIX, the lib/ name indicates where the binaries of static and dynamic/shared libraries reside (not their sources). I use a simple sh script called mkprj <project-name> to create that folder hierarchy for a new project.
- deleted 3y ago[deleted]
- o11c 3y agoCompiling with `-Werror=missing-declarations -Werror=redundant-decls`, and having a test rule that verifies that every .h file is included on exactly the first line of the corresponding .c file, is very useful at enforcing proper headers. Non-`static` declarations should be forbidden outside of headers. Violating this rule is often the only thing needed to make a project hard to understand. Easy-to-understand projects usually only have to add a few `static`s on unintentionally-exported symbols. For libraries, it is critical that a distinction be made between "header files that will be installed" (which should use the project name as a directory prefix!) and "header files used internally". I put the latter in src/ but practice varies widely. I argue that the flat layout is somewhat harmful (though at least using src/ beats shoving everything in ./), because even small projects eventually grow a distinction between "source files that are part of the main installed project" and "source files used solely for maintenance tooling". Making a distinction between "code that needs to be cross-compiled" and "code that needs to be native" is useful even if you're not planning to do cross-compilation.
- JonChesterfield 3y agoAn alternative to the ritual include foo.h as the first line of foo.c strategy is to compile each header individually, `clang -x c -Wall foo.h -c -o /dev/null` or similar. That catches roughly the same developer intent. Similar to the cross compiled / native distinction, it's probably a good idea to put platform specific things behind an interface up front. Link time is a good point to swap out win32 for linux or similar and implementing the second target is much easier if you drew the line up front. The platform layer might also be one worth writing a mock for as part of testing.
- o11c 3y agoThe problem with `-x c foo.h` is that `#pragma once` isn't legal in the top-level source file. There might be other problems too. Using manual header guards is a bug-prone waste. Nobody has ever demonstrated me a case where `#pragma once` fails to work with compilers from the last decade or two.
- 3y ago
- IYasha 3y ago> build intermediate build files e.g. *.o (created by make) In all these years I've yet to see another person (other than me) to use ramdrive for temporary files. Is everyone okay with slow compilation and SSD rape or am I missing something?
- foofie 3y agoThis sounds like a carbon copy of the project tree layout specified in C++'s pitchfork conventions. https://github.com/vector-of-bool/pitchfork https://github.com/vector-of-bool/pitchfork