How to Structure C Projects: These Best Practices Worked for Me
lucavall.in
lucavall.in
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.
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.
A unit test wouldn't know the calling context of procedures deep in the call graph, and cannot detect unsafe but seemingly working code.
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.
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.
> 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'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].
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.
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...)
> 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.
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.
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...
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.
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.
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.
Do you have any sample projects that showcase both the tests and the code generation? I'd love to take a look.
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...
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.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...
This is why you still see a lot of important C project support autotools even though it's a pretty awful developer experience.
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.
>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.
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.
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: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.
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 :))
Does it? IIRC something like
build/%.o: src/%.c include/*.h
$(CC) -c $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
works well enough for small projects.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...
example: build/blah.o
echo nothing special
.PHONY: example
...requires blah.c to be in build/. 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).
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.
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.
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.
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.
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.
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.
But, there is plenty of excellent documentation available for using build and packaging tools of your choice to work similarly to Cargo.
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".
Make is annoying but Makefile issues are found quickly and easily when running `find . | entr -c make` and saving the source file often.
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.
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.
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.
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.
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.
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.
It's strange that you think this post is you "not engaging".
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.
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.
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.
> 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.
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."
> 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. :-)
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?
> > 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.
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.
> 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.
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.
But, there are similar build and package systems for C. Cargo is nothing special.
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.
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.
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.
> 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.
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.
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.
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?
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.
...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?
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.
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.
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.
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.
There, now we're not limited to just Rust advocates, so I hope you no longer find this thread sad : )
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.
so yes, you pointed out the moniker of the individual doing this. It doesn't obviate the point.
- Rust? Why not Nim?
- Nim? Why not Zig?
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.
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.
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.
Here is a C build system:
cat all.c
#include "foo.c"
#include "bar.c"
#include "main.c"
clang all.cso you can add run-time defines.
But I don't want to solve trivial problems. That's not where the value is.
What about "gcc mycode.c" or "make"? You shouldn't need to reinvent your build system every project.
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 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.
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.
Then each platform gets its own directory with .c files.
ie... platform.h
platform_windows.c platform_linux.c etc.
This way, my build file for windows uses platform_windows.c, etcetc.
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.
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.
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 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
Zig really removes a lot of hurdles in the onboarding process and let you jump into building software right the way.
Just have a look at how many C make replacement exists
Also, being able to cross-compile would be cool. But I guess cosmopolitan solves this.
[0] https://andrewkelley.me/post/zig-cc-powerful-drop-in-replace...
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.
Conan, gdb/DDD, Rational Purify etc. exist and can be replaced/improved by better tools.
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.
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.
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.
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?
Just remove best-practices, it's a silly phrase.
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
[0] https://xmake.io
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.
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