Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
How to Structure C Projects: These Best Practices Worked for Me (lucavall.in)
180 points by ingve on March 8, 2024 | hide | past | favorite | 157 comments


I 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.


People love ceremony. All the source files are written in lists in cmake files. [...] I love writing C because I do none of that overhead. The files to compile are whatever is on disk.

I couldn't agree more. I can't stand build tools that don't take globs! I shouldn't need to list the same thing in multiple places just to make it work. Almost all the time, the files on disk should be the only list needed.


The reason why people write all the source files in a list in a cmake file is that it means that when one developer adds a new file, everyone else's build system automatically regenerates the next time they try to build the project. When you use globs (which cmake does support), it means that the CMakeLists.txt file doesn't get modified when adding a new file, so the cmake generated makefiles/ninjafiles/whatever have no way of knowing that the build was modified. Instead of having the build system regenerate and then the build go through, they get a linker error and have to manually run "cmake -B build" or whatever to fix it.


CMake can detect when new source files are added and rerun itself. You need to know how to spell that option when globbing though.


From the cmake documentation:

> The CONFIGURE_DEPENDS flag may not work reliably on all generators, or if a new generator is added in the future that cannot support it, projects using it will be stuck. Even if CONFIGURE_DEPENDS works reliably, there is still a cost to perform the check on every rebuild.


Somehow this isn't a problem with Cargo.toml, however. What's cmake missing?


> What's cmake missing?

Where to begin... Maybe let's start with "a build system".

(I'm serious, CMake isn't a build system and doesn't have one. It supports a few...)


What are the alternatives?


I wish I new any good ones. Using CMake myself.


Is this much of a problem if you just assume the mentality of having to run cmake every time you pull code?


CMake supports globs. It just has notes in the reference docs about why that can lead to slower build times or even incorrect builds (like undetected new files) in some cases.


> embrace code generators.

You also don't need to limit yourself to the C pre-processor. There are other dedicated pre-processing languages, like M4, and templating languages you can use.


Definitely don't limit yourself to the C preprocessor. That way lies a tarpit. M4 took several months off me, it can definitely be used to do things but maybe shouldn't be. A really bad play was generating C++ from cmake as a compromise that delighted noone.

Python is good at generating C source. I changed to mostly using lua five years ago as python climbed the complexity curve. Sometimes the right thing is C itself, e.g. laying out data based on sizes of various C types. Currently I'm trying to use xslt to emit syntax trees that pretty print to get the generated source in what is probably a dead end.


Sort of happy to see m4 mentioned, but for the uninitiated: m4 is the receiver of an awful lot of hate, because things can get unwieldy quickly. Go in with your eyes open.


I'm curious how your build process works with the codegen approach. Do you have the codegen run a pass everytime you build the project? Or is it a manual process?

I've always wanted to use Lua to generate boilerplate C code (like typesafe vectors) instead of relying on macros, so might actually start thinking in this direction going forward.


Not the person you asked, but this sort of thing is pretty easy with templating languages, such as Perl's Template::Toolkit[1] or Python's Jinja2[2].

The advantage of a system like that over a plain programming language (such as Lua you mentioned) is that there's less escaping and string.format(xxx) sorts of things. The output text is the primary thing and the templating language has its own special syntax to "activate" it when you need it, rather than the other way around.

That being said, Lua's square-bracket strings are pretty good for bulk text, too (:

[1] http://template-toolkit.org/about.html

[2] https://jinja.palletsprojects.com/en/3.1.x/templates/#synops...


Template-Toolkit seems like the right tool for the job. Thanks for sharing.


Instead of code generation as a special case, let it be the default. That kills most of the complexity.

src/ contains lots of code in lots of languages, all describing code generators of varying complexity. src/foo.c uses 'cp' as the code generator. src/bar.c.py uses 'python'.

gen/ contains the result of the ad hoc code generators. Same tree layout as src, one to one map from those files. All of that is C. Files that get #included and don't make sense standalone I tend to suffix .data, e.g. src/table.data.lua runs to create gen/table.data.

obj/ contains all the C compiled to object code or llvm bitcode, depending on the toolchain.

I originally did that with the idea that I could distribute gen/ as the source code for people who wanted to build the project without dealing with the complexity of the build system, strongly inspired by sqlite's amalgamation. Sqlite is built from a chaotic collection of TCL and C code generators but you don't see that as a user of sqlite.c.

It turns out that debugging the stuff under gen/ when things go wrong is really easy. Valgrind / gdb show you the boring code that the generators stamped out. So in practice I keep that separation because it makes it easier to trace through the system when it behaves unexpectedly.

Lua does that really well. Good multiline string literal support. A template string that you call gsub() on a few times then dump to stdout is very quick to put together. You've got string format for more complicated things, maybe using one of the string interpolation implementations found on their wiki.


In procedural C land, unit testing is nowhere near as relevant as it is in memory-safe OO lands. Assertions are far more relevant. You want to detect illegal program states in development as early as possible. In C11, you also have static_assert for things like "is this static lookup table as big as needed".


I get why assertions would be more important than in memory safe languages, but why are unit tests less important in C than in higher-level languages? Or you just mean relative to assertions? (which to me don't really influence each other in that way)


Because the coding style is different. The basic unit of code is the procedure, not abstract entities like DateTimeCalculator or EmployeePayrollManager. Procedures are sensitive to the context where they are called. A procedure calling another procedure usually takes care to provide valid input data. Robust error-checking must be in place, or your program is unsafe.

A unit test wouldn't know the calling context of procedures deep in the call graph, and cannot detect unsafe but seemingly working code.


This is more of a culture surrounding the language than anything else. Modern C avoids global state, prefers directly specified context and even allocation is external to the function's logic.

And testing functions is easy.

This is as opposed to procedures, I.e. functions that operate on global state with fragile conext assumptions, which almost impossible to replicate in test environments.


> The basic unit of code is the procedure, not abstract entities like DateTimeCalculator or EmployeePayrollManager.

I agree, but in almost any non-trivial well-run C project the basic unit is going to be datatypes, opaque pointers, modules, etc.

In that case I generally throw in a `bool MyObjectType_test(void)` into the module so, in some sense, I'll have some basic sort of testing. Not as nice as the OO languages provide, but enough to give myself some confidence that changes don't introduce blatant bugs.


I work mostly with functional programming languages where the function is the basic unit of code, what you say is true but is also true for "abstract entities". Not all classes need robust error checking if you control how they are being called.

> The basic unit of code is the procedure

I think ultimately this is where we think differently. If you are okay testing a whole class in an OOP context and calling that a unit test, then a test that calls multiple procedures (because one of them creates the input for the main procedure I want to test, for example) can also be considered a unit test.


I also think because it's lower level what functions are built on tend to be long term stable. Function was tested at one point and then used in production for a couple years. It works and will continue to work as long as no one messes with it.


> In procedural C land, unit testing is nowhere near as relevant as it is in memory-safe OO lands.

I'm not sure why you'd say this. Testing has more to do with the developer than the language. The SQLite project is written in C and has 590 times more test code than source code [1].

[1] https://www.sqlite.org/testing.html


> 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.

Do you have any sample projects that showcase both the tests and the code generation? I'd love to take a look.


The tests look much like https://github.com/JonChesterfield/EvilUnit/blob/master/evil.... That's from the self tests for the test framework. It's syntactically modelled on catch2. The name is because I built the thing out of preprocessor macros to run on freestanding c89.

The projects using code generation in that fashion are proprietary. There's a makefile at https://github.com/JonChesterfield/boring-makefile/blob/mast... set up to do the src/gen/obj codegen by default structure.

Just made the ad hoc xml codegen hackery public, some examples of tests written a couple of days ago at https://github.com/JonChesterfield/xml/blob/main/tools/intse...


Wild, I was just thinking about how I wanted to do reflection on C structs using LuaJIT FFI for serialization.


It'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.


The 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.


> ...not least of which being that it relies on dependency fulfilment order which can be non-deterministic (e.g. if you pass -j).

Didn't actually know that was a thing until I accidentally ran into it yesterday when I ran the make command in the wrong terminal window and the build was very, very unhappy.

At least it didn't bork my entire /home directory like that time we don't talk about...

My general rule is Make for simple things and CMake if it involves distro-packed library deps or I'm building python modules with complicated enough builds that I don't wan't to beat the dead horse of distutils into submission.

Honestly, if I can't tell what the build system is doing without having to ask google then it's way too complicated.


You can consider using MinUnit and Clang-Tidy together with Clang-Format as well, further, for C it should be something more like this:

    CC = clang
    CFLAGS = -g -std=c2x -Weverything -fsanitize=undefined,address
    
    all: app.exe
    

    app.exe: src/main.c
      $(CC) $(CFLAGS) src/main.c


  clean:
    rm -rf /build

See my comment here with an example of an NMAKE Makefile. Consider:

https://nullprogram.com/blog/2017/08/20/


> 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-Example...

And Conan

https://github.com/Release-Candidate/Cxx-Buck2-Conan-Example...


If 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.


Calling rust a high level language gave me a chuckle. Thank you sir and incase anyone wonders, I agree.


They said high_er_. It's not controversial to think that C++ and Rust are higher-level than C.


It seems weird to me to write your own build system and give advice on build systems when your basic makefile already completely violates what the manual suggests.


Unnecessarily vague and uncharitable comment but I'll bite.

While I recommend anyone who uses GNU make should read the manual, the manual is full of outdated best practices which can be safely ignored.

If I was going to use this makefile for my own project, I would even drop the clean target entirely. phony targets are mostly a misfeature. Although it looks like the makefile as accepted by the project hasn't marked the all target as phony.

But go on, tell me what it is that is not recommended specifically. I am aware of many deviations from what the GNU make manual claims is recommended. A large portion of deviations is the lack of many of the standard targets it thinks should be provided. Another deviation might be the uppercase OBJS should be lowercase according to the manual, but in this case I was trying to minimize the number of immaterial changes from the original makefile I modified to arrive at this point since it was not my project.


No need to talk about anything advanced. Line 2 and 3 for example make it impossible to override certain behavior you’ve set.

>If you are insistent on make, stick to GNU make, read the GNU make manual, understand make

I refer to this authoritative sounding comment while you miss out on the very basics like that.


>Line 2 and 3 for example make it impossible to override certain behavior you’ve set.

This is intentional.

It's not possible (unfortunately) to allow users to simultaneously be able to override a variable OR add to a variable in make. In the case of the FLAGS variables I usually prefer to append to allow users to add to the variables. As a compromise, in my own makefiles I explicitly ensure that these variables _only_ contain flags which are mandatory for the project to build correctly. In this case this was deviated from (with the warning flags, -fsanitize=address, -ggdb) because of the request of the project author.

Would you rather an end user be able to quickly add LDFLAGS and CXXFLAGS or rather have the end-user have to copy the entire list of flags out of the makefile just so they can add one?

I would say it's more common for someone to want to add a flag than to replace all the mandatory ones. If someone is using a sufficiently different compiler such that those flags are no longer relevant, they would likely need to modify the rest of the makefile.

>I refer to this authoritative sounding comment while you miss out on the very basics like that.

It's uncharitable to assume that just because you don't understand a decision someone made in something they wrote, that they must be ignorant about the tool they are using.


I've always preferred Stephen Brennan's approach mentioned here: 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 :))


> 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.


I 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).


You 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...


Doesn't the built-in rule put files in the wrong folder? That is, the same folder the source file is in?


Yes; 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/.


You can probably use VPATH as a hack


This (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.


Every C discussion on the net always gets derailed by Rust proselytising.

No other language community does this to other language discussion. Even C++ fanatics didn't do this back when C++ was being sold as a C replacement.

This is an entirely new and somewhat sad, phenomenon: the need for the rust community to derail any and all discussion about some other language reeks of a community that feels threatened.


The comment you're responding to wasn't written by "the rust community", it was written by kerkeslager, who has written a bunch of C programs, but has found that the Rust build tools are better. There is nothing new or sad about it.


It turns out, the rust community is made up of individuals. No one made the claim that there's some single entity called the rust community that's doing this. It's the individuals w/i that community doing it.

so yes, you pointed out the moniker of the individual doing this. It doesn't obviate the point.


> The comment you're responding to wasn't written by "the rust community"

This makes no sense. Almost by definition, anyone derailing a discussion to mention $FOO is a member of the $FOO community.

It's disingenuous to suggest that all derailment is due to non-$FOO advocates.

> There is nothing new or sad about it.

The phenomenon of persistent derailment is, in fact, something that I had not seen until Rust arrived.

It is new, it's limited only to Rust advocates as a group, and I find it sad.


> The phenomenon of persistent derailment is, in fact, something that I had not seen until Rust arrived.

Apparently you have never seen a thread on X11, which almost invariably is derailed by a discussion about Wayland. Or in general, any discussion on Wayland or systemd is derailed into a rant how they're the worst software ever developed or something, to a tedious degree that makes me long for the Rewrite-it-in-Rust comments because at least those sometime make compelling points.


I also find that Go has delightful build tooling; running 'go build ./...' is nearly the universal build tool.

There, now we're not limited to just Rust advocates, so I hope you no longer find this thread sad : )


Agreed, go has good build tooling. And pretty good tooling in general.

And if you search my post history, you'll find some posts that make it clear I despise go.

As it turns out, languages aren't religions--you can like a language and admit it has flaws, and you can hate a language and admit it has strengths.


1. I've written, at most, 10,000 lines of Rust. Calling me "the Rust community" is quite a stretch.

2. For comparison, I've almost certainly crossed the million lines of C mark years ago--if anything, I'm the C community.

3. You and I remember very different things about the C++ community. In fact, I also remember a number of other languages being offered as alternatives to C over the years including some obvious ones like Pascal or Basic, and also including some real absurdities like Lua (nothing wrong with Lua, but it's really not for the same kinds of projects as C). The idea that comparing your language to C is some sort of new phenomenon, is flat wrong.

4. You're ascribing a lot of intent to me bringing up Rust which isn't there. I'm not trying to derail the discussion--if you want to talk about C, do it.

5. The idea that the Rust community is threatened by C is a bit bizarre.


Every <lang> discussion here gets sidetracked/sidethreaded by <lang2> mentions.

- Rust? Why not Nim?

- Nim? Why not Zig?


Your 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.


> 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.


Right, crucially cargo is in the box with your Rust compiler, you have to go out of your way to have just the actual Rust compiler but not cargo.

As with the lesson gofmt taught, you must supply the basic feature in the box. Humans are lazy, so if it doesn't come in the box only people who know they can't live without it will even bother, put it in the box and only people who can't live with it will remove it.


That's why we need a new build system / package manager for C. /s (but not really)


https://xkcd.com/927 comes to mind.


Yeah, that's the joke. But I mean, you have to admit the status quo isn't that good. Something really very much better might be able to claw its way to the top.


Please perish the thought.


But C ships to all sorts of places, so why would C limit its options by limiting its support to its own package manager?


How would having a builtin build system (I'm not even asking for a package manager) limit where C can ship to?


> Your problems are not with C the language but with the chosen build tools.

That's not a distinction I care about. The tooling around a language should be a part of discussing the merits of a language. Your language can be the most ergonomic language in the world, but if all your development gains are lost fiddling with build tools, you haven't gained anything.

> Have you tried using a more modern tool like Meson?

I haven't, but now that I know about it, I'll give it a try. Thanks for the info! It looks promising.

> 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.

Maybe, but that just brings up the problem that there isn't really a clear second-tier system outside the standard library for tools. Certain tools (i.e. Valgrind) are well-known, but there are a million different ways to install a million different tools and for most things it's ultimately more common to just roll your own.


With make, you don't have to install anything, it's POSIX


This technically isn't true, but I'll give you the point because I don't see `sudo apt install build-essentials` as a significant barrier to entry.

But being able to print "make: ** No targets specified and no makefile found. Stop." to stderr isn't particularly useful, and having to write a Makefile is a significant barrier.


Being POSIX doesn't mean it's included by default; for example, you can start with the default installation of debian, install a compiler, and still not have make installed


I don’t understand why people bother with complex build systems for small C projects.

Here is a C build system:

    cat all.c
    #include "foo.c"
    #include "bar.c"
    #include "main.c"

    clang all.c


#include "/dev/tty0"

so you can add run-time defines.


Now add an address sanitizer pass, reasonable unit tests, and wire it up to CI so you don't accidentally break things or trash memory.


Sure, trivial problems only require trivial solutions. Obviously.

But I don't want to solve trivial problems. That's not where the value is.


It is true that C does not have a single tool enforced by the language platform.

But, there is plenty of excellent documentation available for using build and packaging tools of your choice to work similarly to Cargo.


In practice this comes off as a rather motte and bailey argument. "There's plenty of ways to make building C as easy as Cargo." OK, show me. "Well, you use this and do this and this." OK, that didn't work because of $THIS_REASON. "Oh that's easy to fix you just do this thing." {Repeat several times.} OK, I guess it's working now, but there's no way that was as easy as cargo. "What are you talking about, it was a series of easy steps that only involved reading dozens of pages of docs, a couple of not-quite-current-but-close-to-accurate Stack Overflow posts, upgrading to the correct version of the build tool, and directly posting on a couple of mailing lists!"

To be honest, the moment I have to evaluate a dozen tools, pick one, and use it, even if that tool does manage to be "$TOOL build" and literally nothing else which I don't think is anywhere near something people can count on in the C world, it has already proved to be harder than "cargo build".


First, I did not say anywhere in my reply, "as easy as Cargo". I said "similar to Cargo." Huge difference.

C has been around for over 50 years. It predates many of the modern "everything including the kitchen sink" style of language platforms. It has spread across so many platforms and uses that a Cargo like integration is not going to happen.

But, this does not mean that build, package, and install tools don't exist. If you want me to provide you with a single tool to rule them all, well, that's impossible for the reasons I listed above. But, depending on which platforms you care about and which features you want, there are plenty of existing tools.

If your goalpost is "change C to be like Rust and have a single build tool", then that's impossible. If your goalpost is "find me a tool that does X, Y, and Z", then there that is certainly possible. There are Cargo clones for C/C++ that work just fine.


While less aggressively "motte & bailey" then the sibling reply from PH95VuimJjqBqy, you basically proved my point rather than disproving it. You can't name a single tool that is as easy as cargo. I know that's because if you do there will be a dozen replies from users of that tool going "Oh, my, no it was quite difficult to set up, ultimately didn't work for us, and we had to abandon it for this other tool."

I do not understand the mindset that believes that if you explain why a problem is hard, it is somehow no longer hard, or somehow it no longer "counts". Yes. C is a sprawling multi-decade monster that never started out with a good build tool because it was just too early in history for that to be the sort of concern it is now. I certainly wouldn't dream of claiming there aren't reasons C is complicated to build, even ones that are generally good when you consider the entire history of the project. The world would not be better off if we had somehow waited for C to be perfect before using it, because it is precisely the act of using C that has taught us the ways to improve other languages. I credit the platform for that, I do not deduct points.

However, none of that changes the fact that it is a bear to build anything non-trivial in C, and the simple act of reading a list of your tool options is literally already harder than "cargo build". I basically don't believe by assertion that there are tools that can solve this with just a relatively dainty (by C standards) little declarative file and a single command execution. I'm sure they have a "happy path" that looks like that but as soon as I leave it it'll explode in complexity, and I'm basically guaranteed to leave it at some point for any non-trivial project.


I'm going to recommend everyone ignore this poster, just don't reply to them.

They're not going to accept anything other than "yes, you're right jerf" and any attempt to do so is going to be met with squawking about motte and bailey fallacy because they believe internally they've found the perfect defense.

it's not worth engaging.


So basically you recommend everyone ignore them because they disagree with you?

It's strange that you think this post is you "not engaging".



If you don't think it's helpful to engage, put your money where your mouth is, and don't engage, i.e. stop posting.

Note I'm saying if you don't think it's helpful to engage. I don't have any objections to you posting. It just makes you look a bit silly to keep engaging while claiming that engaging isn't worthwhile.

I'm really not sure what relevance you think that link has--I'm not seeing any particular intolerance happening here. Disagreement != intolerance.


I'm not surprised you don't understand the point.


If you're going to put words in my mouth, then there's no need for me to respond any further.

If you are genuinely curious about build systems in any language, I can certainly help you. But, if this is the "Cargo is the best vs all others" debate, kindly count me out. I don't care. Cargo is fine if you're doing Rust work. If you're using other languages or platforms, there are plenty of excellent alternatives.

This sort of Rust proselytizing -- on an article about C of all things -- is why so many perceive the Rust community as being toxic. You might think you're helping to spread the glory of Rust, but that's not how you are coming across.

Note that I'm not even comparing the merits between the two languages. If you like Rust, write software in Rust. If you like building your Rust projects in Cargo, have at it.


> But, if this is the "Cargo is the best vs all others" debate...

It isn't. It's certainly better than any C build systems, but I'm aware of a few other similar build systems (just not for C).

> ...kindly count me out. I don't care.

Only you have the power to stop posting. And if you did, it would make your claim that you don't care a lot more convincing.

> If you're using other languages or platforms, there are plenty of excellent alternatives.

True! But C isn't one of those languages.

And let's be real, that's really why you're mad. It isn't that I'm bringing up Rust that's making you mad, it's that I'm criticizing C.

C isn't perfect and it's not the best at everything. And that's fine! It's great for what it is. But what it isn't is a modern language with modern tooling. And that's a real downside to consider when choosing a language to write a project in.

That's not the only consideration, and there are a lot of reasons why I, myself, would choose C for a new project (not the least of being that I'm a lot better at C than at Rust).

> This sort of Rust proselytizing -- on an article about C of all things -- is why so many perceive the Rust community as being toxic. You might think you're helping to spread the glory of Rust, but that's not how you are coming across.

I'm not particularly concerned with how I represent the Rust community. I don't even view myself as part of the Rust community (yet). If anything, I have a lot more claim to being a member of the C community than of the Rust community, as I've written orders of magnitude more C code than Rust code.


> > But, if this is the "Cargo is the best vs all others" debate...

> It isn't.

You could've fooled me with how uncharitable you are being here by blowing comments way out of proportion.

> Only you have the power to stop posting.

Full context: "But, if this is the "Cargo is the best vs all others" debate, kindly count me out."

Using ellipses and quote mining to make your opponent look foolish is the lowest form of sophistry and against HN policy. There isn't a "debate" to "win" here. So, why go to this sort of trouble over nothing?

> > If you're using other languages or platforms, there are plenty of excellent alternatives.

> True! But C isn't one of those languages.

I fail to see what C has to do with the choice of build tools. C isn't a build tool; it's a language.

> And let's be real, that's really why you're mad.

Who is mad? Literally, I replied with a comment about build tools. You keep trying to have the debate you want here by inserting words in people's mouths. It's disingenuous.

> C isn't perfect and it's not the best at everything.

No one. NO ONE, has made any claims even remotely like that. Yet another silly strawman for the debate you desperately want to have here.

> But what it isn't is a modern language with modern tooling.

Actually, there is excellent modern tooling for C. There are proof assistants and model checkers. It's possible to do similar things in modern C as in Rust or other languages, from proving the absence of UB to proving memory safety features. But, seriously, no one is having this debate, so it's silly. We were talking about build tools...

> I'm not particularly concerned with how I represent the Rust community.

Good, because these silly "debates" -- over a build tool that has somehow mushroomed into a C vs Rust debate in only your head and no one else's -- might seem cool to you, but are counter-productive.

> as I've written orders of magnitude more C code than Rust code.

And yet, you've never done any reading about modern build tools or package systems with C support. That's a mighty shame.

By the way, did you know that you can build C/C++ projects using Cargo? Yeah. Me neither. Huh.


> Full context: "But, if this is the "Cargo is the best vs all others" debate, kindly count me out."

Okay, then my response to the full context is:

This isn't the "Cargo is the best vs all others" debate.

If you wish to be counted out, you can stop posting at any time.

You'll note that this is basically what I said to your post when I was "taking it out of context", with the exception that I also responded to some context you decided to leave out when you quoted yourself.

> Using ellipses and quote mining to make your opponent look foolish is the lowest form of sophistry and against HN policy.

I split up your comments when I quoted them to make it clearer what I was responding to.

> I fail to see what C has to do with the choice of build tools.

You are definitely smart enough to figure that out.

The author of the original article seems to see the connection, given they titled their article "How to Structure C Projects" and then went on to write a bunch about a build system.

> > C isn't perfect and it's not the best at everything.

> No one. NO ONE, has made any claims even remotely like that.

Sure, nobody is foolish enough to make such a claim so obviously. But if a person responds to any criticism of C with vitriol, one begins to think that person is invested in a belief that C is perfect.

> Actually, there is excellent modern tooling for C. There are proof assistants and model checkers. It's possible to do similar things in modern C as in Rust or other languages, from proving the absence of UB to proving memory safety features.

And I'm sure these modern tools are easy enough to use that everyone uses them, given their obvious benefits, right? Have you even used a proof assistant?

To be clear what point I'm making: these tools are amazing, feats of engineering, but they're not really viable for most C projects. Using a proof assistant is far harder than you're giving it credit for, enough so that I very much doubt that you've one to prove things about C programs. If you have, you're much smarter than me, because I've tried, and proving anything non-trivial was beyond what I could do with reasonable effort.

> By the way, did you know that you can build C/C++ projects using Cargo? Yeah. Me neither. Huh.

If I go into a average Rust project, and run "cargo run", Cargo will build the project, and the resulting binary will run.

If I go into a random C project, and run "cargo run", that won't happen.

And in fact, there is not a tool in existence where I can go into a random C project and run that tool, and it will build the C project. "make" is about as close as you can get to that, and in most C projects that simple four letter command is glossing over a whole lot of work that went into producing the Makefile.

I'll remind you of when you said, "I fail to see what C has to do with the choice of build tools."


> > I fail to see what C has to do with the choice of build tools.

> You are definitely smart enough to figure that out.

Clearly not, because the features you enumerate with respect to Cargo aren't unique to Rust or non-existent in C. They are simply build tool features. If you learn what these features are called, you can search for them in other tools, assuming that's not too much "effort" for you.

> But if a person responds to any criticism of C with vitriol,

So far, the only people in this thread replying with vitriol are you and jerf. Believe it or not, I was trying to be helpful, and somehow you keep trying to rope me into a dumb Rust debate that NO ONE CARES ABOUT.

> Have you even used a proof assistant?

Only daily. But, let's keep those unfounded assumptions coming...

> To be clear what point I'm making: these tools are amazing, feats of engineering, but they're not really viable for most C projects.

Ah, but CBMC is completely viable for any C project, and proof assistants are very much on the way toward viability for general adoption right now. I should know, because that's kind of an area of specialty for me.

> If I go into a average Rust project, and run "cargo run", Cargo will build the project, and the resulting binary will run.

> If I go into a random C project, and run "cargo run", that won't happen.

Now we get to the crux of your issue. You want a zero-configuration build tool. They exist for practically every language and platform. Search for that phrase and be enlightened. But, just like cargo, you'll still need to add configuration for customization (or pass it on the command-line).

> And in fact, there is not a tool in existence where I can go into a random C project...

Note that this is a proper use of ellipses. See above: it's called a zero-configuration build tool. Your ignorance of their existence should not be construed as evidence of cargo's uniqueness.

> I'll remind you of when you said, "I fail to see what C has to do with the choice of build tools."

The reminder is rather silly, as we've just exposed that you haven't put in the "effort" to understand the tools that exist for C, despite having over a million lines of C under your belt. :-)


> Clearly not, because the features you enumerate with respect to Cargo aren't unique to Rust or non-existent in C.

You literally said that one of the features I want from a build system isn't possible in C. You said, "None of them _could_ be the standard, because C is used on thousands of different platforms."

Having a standard build system is extremely useful.

> > Have you even used a proof assistant?

> Only daily. But, let's keep those unfounded assumptions coming...

I won't say I haven't made some incorrect assumptions in this conversation, but in this case, I assumed nothing; I asked a question.

This is the sort of thing I'm referring to when I say that you respond with vitriol, by the way--this sarcasm wasn't necessary to make your point.

> > To be clear what point I'm making: these tools are amazing, feats of engineering, but they're not really viable for most C projects.

> Ah, but CBMC is completely viable for any C project, and proof assistants are very much on the way toward viability for general adoption right now. I should know, because that's kind of an area of specialty for me.

Well, I genuinely hope you succeed, and maybe you will in the near future, but from the perspective of someone trying to use proof assistants, I'm telling you, you haven't succeeded yet. Maybe it's because the best proof assistant isn't publicized enough for me to have found out about them, maybe it's because the problem is intractably hard, I'm not sure.

I also suspect you're a bit overstating the viability of CBMC, but it's been a few years since I looked at the CBMC tools available so I'll grant the possibility there have been strides taken since then, and if this Rust thing doesn't work out maybe I'll take another look.

> See above: it's called a zero-configuration build tool. Your ignorance of their existence should not be construed as evidence of cargo's uniqueness.

Cargo is a zero configuration build tool, but it's not just a zero configuration build tool.

Notably, zero-configuration build tools don't do what I described--you condescendingly (vitriolically?) assumed I haven't tried zero-configuration build tools for C. I have, and they do not do what I described--you have not at all responded to what I said.

There does not exist a tool that you can go into an average C codebase and run it and expect reasonable results, because C codebases aren't all built with the same assumptions--for a zero-configuration tool to work reliably you have to build from the beginning with the same configuration assumptions as that tool. And even if you do that, the "zero configuration" falls apart as soon as you introduce a dependency, because they likely didn't make the same assumptions as the ZC build tool.

The benefit to having a build system like Cargo that is built in tandem with the language and everyone writing the language uses it, which means that everyone using the language writes it with the same assumptions as the zero-configuration build system. This means when you stumble upon a random project in that language, you already know how to build it without reading a lot of documentation.

And as an aside: "go build" works for Go just as well as "cargo build" does for Rust, for the same reasons. You're the only one making this a Rust vs. C debate. I wasn't going to respond to that because I'm trying (perhaps unsuccessfully) to focus on the technology discussion, but I'm a bit tired of having to skip past you (vitriolically) ranting about how toxic Rustaceans are attacking C every other paragraph.

> The reminder is rather silly, as we've just exposed that you haven't put in the "effort" to understand the tools that exist for C.

I won't claim to know every build tool that exists for C, but I will say that I've looked into plenty in my time.

As for your snidely (vitriolically?) disparaging my not putting effort into learning tools: I actually put a lot of effort into learning tools, but I insist that effort not be wasted. There are a lot of reasons learning a new tool is not a good idea. Will it be maintained 5 years from now, or am I learning a skill that will soon be useless? Is it widely used, or will I only be able to use it in niche situations? Is it stable?


> > > Have you even used a proof assistant?

> > Only daily. But, let's keep those unfounded assumptions coming...

and

> This is the sort of thing I'm referring to when I say that you respond with vitriol, by the way--this sarcasm wasn't necessary to make your point.

I see. Sarcasm and ill tone is fine when you use it, but not when I respond to it in kind. I'm not interested in Sealioning. Goodbye.

Post whatever replies you want and "win" this pointless "debate". I don't care to play this stupid game. Good day.


> Sarcasm and ill tone is fine when you use it, but not when I respond to it in kind.

Not at all. Use all the sarcasm you want; I'm a big boy and I can handle it.

And I'm not denying I used some unnecessary sarcasm myself.

But you said, "So far, the only people in this thread replying with vitriol are you and jerf." And I just wanted to make you aware that that's not quite correct.

You're not the innocent victim in this conversation that you think you are.


Sorry... had to come back for one last reply.

> But if a person responds to any criticism of C with vitriol, one begins to think that person is invested in a belief that C is perfect.

That was before I started responding in kind.

> You're not the innocent victim in this conversation that you think you are.

Oh, I was. You and jerf certainly started the rude behavior. Expecting me to sit back and take it, and then pouncing on me for "vitriol" for responding in kind is... wait for it... SEALIONING.

https://en.wikipedia.org/wiki/Sealioning

Push the envelope, then call the other out for pushing back, as if you're the injured party.

Either way, this is far outside of acceptable behavior on HN. I'm disengaging for good this time. I offered you useful advice. You tried to turn this into a silly Rust debate that no one wanted. Then, you act like the aggrieved party because no one wants it.


> If your goalpost is "change C to be like Rust and have a single build tool", then that's impossible. If your goalpost is "find me a tool that does X, Y, and Z", then there that is certainly possible. There are Cargo clones for C/C++ that work just fine.

My goal is to be able to build my project with as little effort as possible. "Effort" includes evaluating different tools and reading their documentation.

When selecting a language to write a new project in, the history of the language isn't something I care about.


If your goal is "I want to use Cargo and nothing else because that would require effort to learn to type a different command", then use Cargo.

But, there are similar build and package systems for C. Cargo is nothing special.


> If your goal is "I want to use Cargo and nothing else because that would require effort to learn to type a different command", then use Cargo.

That's not my goal, and you know that.

> But, there are similar build and package systems for C.

The reason you aren't naming one is that they obviously aren't similar in ways that matter.


> That's not my goal, and you know that.

Didn't you just say that you didn't want to read about any other build system, as that would involve effort? Uh... okay.

> The reason you aren't naming one is that they obviously _aren't_ similar in ways that matter.

Or, as I said previously, I'm not interested in endorsing build tools I haven't used, because I'm not in the market for a cargo clone. They exist. Plenty of folks I know are happy with one or another. Build tools are not difficult to write. As with the article, if you don't like existing build tools, spend an afternoon writing your own. It's a little graph theory and job management.


> Didn't you just say that you didn't want to read about any other build system, as that would involve effort?

No, I did not say that, and you also know that.

> Or, as I said previously, I'm not interested in endorsing build tools I haven't used, because I'm not in the market for a cargo clone. They exist.

If you've not used them, then your confidence that they are equivalent to Cargo comes from where?

> Build tools are not difficult to write. As with the article, if you don't like existing build tools, spend an afternoon writing your own. It's a little graph theory and job management.

I'm the author of the Fur programming language, which does need a package management system, so I will be doing this at some point. But I very much doubt that it will be as easy as you claim.


> > Didn't you just say that you didn't want to read about any other build system, as that would involve effort?

> No, I did not say that, and you also know that.

and

> My goal is to be able to build my project with as little effort as possible. "Effort" includes evaluating different tools and reading their documentation.

Apparently, I don't know that, or I'm being trolled. What even is knowing something when folks contradict themselves two replies in?

> If you've not used them, then your confidence that they are equivalent to Cargo comes from where?

The endorsements of others who have used the tools. But, I'm starting to suspect that you wouldn't consider any endorsement of any tool other than cargo as valid...

> which does need a package management system, so I will be doing this at some point. But I very much doubt that it will be as easy as you claim.

Note that I said "build tool". A package management system is a hair more complicated. You'll need a week instead of an afternoon.


> What even is knowing something when folks contradict themselves two replies in?

You quoted two bits and are claiming they are contradictory, but they aren't.

I said, "My goal is to be able to build my project with as little effort as possible. "Effort" includes evaluating different tools and reading their documentation."

That's not "I don't want to read about any other build systems besides Cargo."

I'm happy to read about other build systems. I would be extremely happy if I found one as easy as Cargo for C, but I'm quite confident that doesn't exist.

But it took me all of 3 minutes to learn how to do everything I need to do with Cargo, and it was simple enough that I have it committed to memory and never have to read it again. I don't want waste time reading about build systems that are immediately obviously harder to use and don't do anything more, because that is a complete and utter waste of time.

And I say "You know that" because it's not some puzzle if you were looking to understand what I say instead of attack it.

> Note that I said "build tool". A package management system is a hair more complicated. You'll need a week instead of an afternoon.

Your confidence in me is charming.


There is a price to easy. In my experience, cargo build times tend to be soulkillingly long. But I don't like waiting more than a few hundred ms for incremental builds. So for me, cargo build is actually harder than make to attain an enjoyable build system.

Make is annoying but Makefile issues are found quickly and easily when running `find . | entr -c make` and saving the source file often.


Build times have nothing to do with cargo but with the language itself. They're not the "price to easy", it's the price of the static compiler checks.


Isn’t it the price of the LLVM backend? That would make it a rustc implementation price.


that's only true if you believe dependency resolution is free.


I have never witnessed cargo being delayed from that by a noteworthy amount of time.

I just tried it, on my PC it takes cargo 0.3 seconds to start compiling the first of 309 dependencies in a clean Bevy repo. The entire compilation takes 31 seconds, and that's the best case with lots of multithreading and all packages already downloaded. That's close enough to "free" for me.


doesn't really matter. You said "nothing to do with" and I said "is not free".

1/3 of a second for a build that has already cached all dependencies means I was correct that it's not free and you were being dismissive when you said it had nothing to do with build times.

If you want to argue that it's a minimal amount of time then argue that instead of what you've been arguing.


I was of course talking about usage in the real world, which is the thing that matters in the end. Harping on the precise meaning of words like that is pointless.


the real world, where nothing means something.


Cargo does not do dependency resolution on every build.


how does a dependency management system ensure the dependencies are there without doing dependency resolution?


By separating the two. If there’s no Cargo.lock, resolution needs to be done, and the result is written out to the lock file. If the lock file is there, then you don’t need to re-run resolution, you just read out the result from the file.

The latency you’re arguing over is probably more to do with rustup selecting the right cargo and exec-ing that than dependency resolution. It’s heavier weight than it should be due to history reasons.


dealing with the Cargo.lock is a part of the dependency resolution.


> To be honest, the moment I have to evaluate a dozen tools, pick one, and use it, even if that tool does manage to be "$TOOL build" and literally nothing else which I don't think is anywhere near something people can count on in the C world, it has already proved to be harder than "cargo build".

C will build and run in places rust can't.

so I should argue C is better because in my chosen metric (diversity of platforms) it wins.


Thank you for that demonstration of the motte & bailey argument.


thank you for being a dismissive jackass.


> But, there is plenty of excellent documentation available for using build and packaging tools of your choice to work similarly to Cargo.

Sure, and there's plenty of documentation for Cargo too. Or at least I assume there is, but I've never had reason to read it.

Do you see the problem with what you're saying?


> Do you see the problem with what you're saying?

No, I don't. At some point, you read documentation, or a tutorial, or example code to use Cargo.

These exist for other build and package systems, some of which are clones of Cargo.

If you want a white glove experience (e.g. cargo new), such systems exist as well for C. Cargo is nothing new.


> At some point, you read documentation, or a tutorial, or example code to use Cargo.

...and the point you're pretending you don't understand, is that the parts I needed were simple enough that I now have it memorized.

> These exist for other build and package systems, some of which are clones of Cargo.

These exist... but you're not going to even mention any, because you're aware that they aren't equivalent. They don't ship with your C compiler, they don't work for every C project without configuration, etc.

I'm open to the possibility that there are better C build tools than the ones I've been using, but they literally cannot be as easy as Cargo because they're hampered by supporting decades of legacy features (and mistakes) of C, the fact that none of them are the standard, etc.

And look, no hate on C. It's done its job for decades and if we're measuring by the problems that have been solved in a language, it's arguably the best language in existence. I've certainly chosen it for a lot of my own projects. But do you really think it's perfect? Are you really unwilling to admit that there is anything newer languages do better?


> ...and the point you're pretending you don't understand...

You're making some very interesting assumptions about what I'm thinking here.

> These exist... but you're not going to even mention any, because you're aware that they aren't equivalent.

Again, you're assigning motives to me based on unfounded assumptions. Actually, I haven't mentioned any because I have no reason to endorse any of them. Not because they are inferior, but because I'm not in the market for a cargo clone. If you were interested in learning more, Google is your friend. If not, well, I'm not here to have some silly debate about how Rust and Cargo are superior to everything ever invented. I don't care.

> but they literally cannot be as easy as Cargo because they're hampered by supporting decades of legacy features

New tools are opinionated. They choose features commonly used in modern software.

> the fact that none of them are _the_ standard

None of them _could_ be the standard, because C is used on thousands of different platforms.

> But do you really think it's _perfect_?

No one, anywhere in this thread, has made this argument or anything approaching this argument. No language is perfect.

> Are you really unwilling to admit that there is _anything_ newer languages do better?

That's another bizarre strawman. No one on this thread has made claims remotely approaching this.

You claimed that you were switching projects to Rust because you don't like C build systems. Taking your comment charitably, I replied that there are build and package tools for C that are literal clones of Cargo. But, now you're making assumptions about my motivations for pointing out this in the comment section of an article about C development and building strawman arguments. Why?

I'm not getting sucked into a Rust debate. I don't care. You like Rust? Use it. You like Cargo? Go for it. But, if the reason why you are using either is because you can't find a similar tool for C development, then I recommend doing the reading. The amount of reading required to switch to a new build tool is significantly less than the amount of work required to port even a medium complexity C project to Rust. Of course, if you're looking for an excuse to use Rust, you don't need to use build tools as that excuse. Just write code in Rust.

Debating this is silly.


> "cargo build", "cargo run", and "cargo test"

What about "gcc mycode.c" or "make"? You shouldn't need to reinvent your build system every project.


`gcc mycode.c` has to be some sort of joke you're making. I'm not trying to compile "Hello, world" in C, I'm writing C code that will be run in production to solve real-world problems.

For projects of any significant size, you're going to run into some constraint which requires you to use `make` differently than you have before.


>For projects of any significant size, you're going to run into some constraint which requires you to use `make` differently than you have before.

That's precisley why official/standard build systems suck, they are extremly cumbersome to wrangle when you go off the beaten path.

So the irony is that standard build systems/package manager are whats good for hello worlds, non-trivial programs require custom build steps.

Just let me write a build.bat/build.sh per platform/compiler/configuration that are explicit and precise in compiler flags, paths, output files, pre/post processing, etc so nothing magical is happening under the hood.


> That's precisley why official/standard build systems suck, they are extremly cumbersome to wrangle when you go off the beaten path.

That is the first sensible criticism I've heard in this thread. Thanks for engaging maturely and with an insight that can only come from actual experience.

My counterargument is that the Make experience is cumbersome to wrangle on projects that are very much on the beaten path. But I see your point that it is flexible, i.e. it doesn't get more cumbersome if you go off the beaten path.

The other caveat I'll give is that my rosy view of cargo may be in part because I haven't written a lot of Rust code.

> Just let me write a build.bat/build.sh per platform/compiler/configuration that are explicit and precise in compiler flags, paths, output files, pre/post processing, etc so nothing magical is happening under the hood.

I agree with this completely, and this is why I avoid what I call "configuration-oriented programming", which is where you're trying to do something that requires a programming language, but you have something like JSON /YAML/TOML instead of a programming language.

But at a glance it appears to me that `cargo` can farm out to the programming language of your choice pretty easily so it doesn't feel like this critique applies.


> `cargo` can farm out to the programming language of your choice

That's precisely why I like Make. Make lets you specify an input file, the name of an output file, and the shell commands necessary to generate the output file from the input file. You have the full power of the shell at your disposal which means you can farm out to core utils, Python, or whatever else suits your fancy.

I not only use Make for compiling C code, but also for compiling my websites (run "go build", minify assets, etc). You could use Make to invoke rustc if you wanted. Make is composable by design. In my opinion it _is_ a scalable build system.


I have the same problem with Rust modules:

e.g., where do I put crate::, vs module X, vs module X{ } again? Do I have to specify it in the automatic lib.rs too? Or ...

C at least lives on the filesystem and you tell it where to grab things.


Module definitions in Rust have simple rules that resolve `mod foo;` into foo.rs or foo/mod.rs, which lives in the filesystem layout just as it does in C, you just don't have to use quotes and it has to form a tree from a root module at lib.rs. If you want to be even more explicit about it there's an attribute you can put on a `mod` item to override the normal path resolution rules.


> e.g., where do I put crate::, vs module X, vs module X{ } again? Do I have to specify it in the automatic lib.rs too?

Sure, but there's one clear answer to that question which you can refresh your memory of in under 3 minutes of reading.

C does live in the filesystem, but where in the filesystem? Oh and it doesn't always live in the filesystem--sometimes it lives in the package system.


Keeping .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.


If 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.


When 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.


Compiling 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.


An 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.


The 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.


I've heard that `#pragma once` fails to work in the top-level source file, such as when compiling a header using -xc to see whether it is independently well formed...


I'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


Just 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.


I sense incoming bikeshedding in the comments.

Just have a look at how many C make replacement exists


Isn'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.


My 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.


Ah 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.


That 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.


The C++ world is keenly interested in having a package manager. I believe there are a few competing ones now.

All I've seen in practice is "vendoring", which amounts to copy the source into your tree with some degree of review or patching, and then go on as before. After being initially indignant that we didn't use libboost from the OS that approach has really grown on me.

Dependency management is a horrendous mess. Moving the logic into package managers hides that mess from the day to day development which is very nice. However, following a debugger into the binaries that came from apt install is not a great time. Patching the source to behave differently while trying to work out what is going wrong is rather harder too.

If you do the vendored thing, you also get the interesting feature where you can checkout the source tree as it was eight months ago to correspond with whatever a customer is asking about and it builds and runs, instead of telling you that the dependencies no longer exist.

I'm now heading further in that direction. All the libraries should be built from source in the same repo, but preferably by tools that are also in that repo. I'm going to end up with a C compiler checked in as well, much like the embedded toolchain people are prone to doing.


Some open source types really hate vendoring on some level (it's hard to say if it's genuinely technical or just an immune system turning on) — not everything supports it that well (e.g. python really doesn't, it's terrible), but when you have it working it's so nice for the things you mention.

We had a build fail for a few days because a git commit with a given hash didn't exist upstream. Solution? Pin to a branch of course! Vendoring as that solution takes a surprisingly deep realignment.


zig cc is a top notch cross compiler that you should check out[0].

[0] https://andrewkelley.me/post/zig-cc-powerful-drop-in-replace...


That’s so cool, It can compile to many different targets!


Nobody 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.


What'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.


> 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?


FWIW practices is misspelled "pratices" twice.

Just remove best-practices, it's a silly phrase.


I'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


Sounds like the perfect theme for a Cookiecutter template.

https://cookiecutter.readthedocs.io/en/stable/


Am I missing something, or are the examples both projects with less than 20 C source files?


In recent times I prefer Xmake[0] to CMake.

[0] https://xmake.io


Start 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.


Use MinUnit.h

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

At least, this is my approach.


Here'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


This sounds like a carbon copy of the project tree layout specified in C++'s pitchfork conventions.

https://github.com/vector-of-bool/pitchfork




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: