Hacker News new | past | comments | ask | show | jobs | submit login
Two types of C programmers (utcc.utoronto.ca)
111 points by susam on April 23, 2023 | hide | past | favorite | 218 comments



I'd consider myself the former: I use C because it's the only viable option for what I'm doing - but most of the time it's nothing to do with C itself, but the various extensions and builtins of GCC. It's the inline asm, the control of registers, placement of code and data, control of inlining, etc, which are missing from all of the C "replacements". The replacements assume you are building a user application on top of an existing unix-like OS or VM, but they're missing the parts necessary to write the OS or VM itself. They don't provide simple ways to use hardware-specific features.

Obviously there is C++ which can leverage most of this too, but C++ traps you into an ABI which is difficult to use from any language which is not C++.


I also consider myself the former. I often use C not so much to stay close to the hardware, but to be closer to the operating system APIs. Of course, there are often API bindings for other languages, but they're often very incomplete, buggy, non-idiomatic, non-ergonomic, or lacking performance. And IME you only find this out after a few months. But from the get-go they're always worse documented and less flexible. I tend to mix C with other languages, like Rust or Python. This is mostly for audio and graphics stuff.

With that said: Swift replaced C (and Obj-C) 100% in the Apple world for me. Because the API bindings are as good.


D has a very nice inline assembler, bitfields, inline control, alignment control, etc.

C's "register" keyword has been ignored since 1990 or so.


"register" is still supported by GCC for combined use with asm. eg, `register uintptr_t x asm ("edx");`

Does D support anything like `__attribute__((section("t1")))`, so we can have the linker decide how to locate some compiled code, or computed gotos?

Can we use D's inline assembler to clobber registers?

I've had trouble even with clang and extended asm. For example, LLVM does not support the "g" constraint.

I understand that there aren't many use-cases for these specific features, but I have an unusual direct-threaded VM which relies on some of them, and the only real alternative I see is to write plain assembly.


I've been using D for OS development and have found it very good for controlling low-level details. GDC is the GCC frontend for D, and has most/all of the same features for controlling this stuff as GCC. For example, you can use `@register("edx") ulong x;` to specify that a variable should be in a particular register, `@section("t1")` on functions to place them in certain sections, and the inline assembler is the same as with GCC. Note: the @register feature is new with GDC 13. See here for the docs: https://gcc.gnu.org/onlinedocs/gdc/.

And of course LDC supports all the LLVM custom attributes, plus GCC-compatible inline assembler (not sure about the "g" constraint though) and LLVM-style inline assembler.


Thanks for the information. Appears that it does support the section attribute from GCC too. I'll have to explore this a bit and see if I can use it, since it seems to have all or most the features I use.

I did learn D some years ago when the standard library situation was not great, but might be worth looking at again.


Depending on how much language support you want, you may want to compile without the D runtime (in which case you only have access to the C standard library, and various features are disabled, such as classes/interfaces, garbage collection, exceptions, and most of the D standard library). You can disable the D runtime in GDC with -fno-druntime and in LDC with -betterC. With those flags, the basic hello world program looks like this:

    import core.stdc.stdio;
    extern (C) void main() {
        printf("Hello world\n");
    }


The register support you describe is an extension, not really part of C. Gcc has lots and lots of C extensions, but they don't make the language simple.

D's inline assembler does register management for you, i.e. it tracks register usage through the instructions, so you don't have to say which registers are read or written to.

D does not support the "section" extension. I understand that certain gcc C extensions are needed for special purposes. Gcc may be the most suitable language if you need those extensions.

What I would do is use gcc for the parts of the code that need those extensions, and D for the rest (D code can directly interact with C code).


Thanks. My root comment mentioned that it was not really the features of C that I depended on, but the GCC specific features.

I'm interested in using D now as sibling comment mentions that I can use some of these features in gcd, but I'm not yet sure how much benefit I'd get over using C by using a subset of the D features which are compatible with the constraints of my VM.


One big feature is D has modules. There's no reason C can't have modules, but they don't. You don't need to code .h files anymore.


LDC and GDC both have the section concept exposed as a UDA.


Good!


GNU jitter right? I’ve seen (a few?) highly detailed slide decks about it. Extremely cool stuff

I found this pdf but I thought there was a different one, anyone link to that?

https://binary-tools.net/jitter-binary-tools-summit.pdf


Not jitter, but I'm familiar with it and have used some of its ideas. (More Jitter slides referenced on Luca Saiu's page:https://ageinghacker.net/talks/#jitter-talks)

The (experimental) VM I'm working on embeds type information into pointers. I place some functions at fixed virtual addresses and use the type information from the pointer to materialize these addresses at runtime, without having to dereference any pointers until I actually call the function. Essentially, if you have a pointer you will know the type of value it points to from the pointer itself.

This method places some tight constraints on how memory can be allocated, but I don't think it will be too much of a limitation for most applications intended to run on it. I have 12-bits in a 48-pointer which provide type information, which leaves a maximum 36-bits of virtual address space per type (or 35 bits if you discount the most significant bit which refers to kernel space).

I'm currently using the section attribute to implement it, but I'm aware there are other methods to achieve this. I could do it at runtime by `mmap`ing the virtual memory and then loading in the machine code at the addresses I need. This method might be more flexible in the long run and would free me up from using GCC specific attributes.


Linux is reportedly written in C and it still has a good share of plain assembler.


> Obviously there is C++ which can leverage most of this too, but C++ traps you into an ABI which is difficult to use from any language which is not C++.

To be fair to C++, all the other languages in this space are as bad at providing ABIs in their own languages. All (almost?) mostly provide ways to declare C ABIs, which C++ also supports.

C++ has rough C++ ABIs, but mostly because it bothers to try in the first place. It's not like other languages really "solved" ABI better than C++ has.

Even C ABIs are a little rough since there's so much preprocessor use to factor in and/or avoid.


C's ABI is that much simpler because it doesn't really place any constraints on the memory layout of your types. With C++ (and others), the values passed around may also have vtables linked to them, so your language must then also provide an in-memory representation of objects which is compatible with the C++ representation in order to do FFI method calls. That places quite a constraint on your language because to do it efficiently you basically want a copy of the C++ object model. So you're developing a kind of C++ sibling language, and C++ is quite complex, and now any other languages which want to interoperate with yours has to do the same.

This is why it's pretty standard to just have a C FFI and provide a C wrapper for C++ libraries to use them with other languages.


Of course the C ABI places constraints on the memory layout of the types. For example, alignment. Also in what register "small" types are passed when passing them to functions. It actually depends if the struct has float members or not. C itself is quite complex already. C++ adds more features.

But for example, the Rust ABI uses the C++ ABI, because C is too simple and doesn't support unwinding (for panics) or 128bit integers


Out of curiosity, have you looked at the ASM inlining options in Rust? https://doc.rust-lang.org/reference/inline-assembly.html


Rust's inline assembly and configurable targets sets the standard going forward.

I just wish their trademark policy relaxed some of the non-trademark related requirements making it a deal-breaker.


What does the trademark policy has to do with this?

Btw the recent fuzz recently was about some proposal and not the actual one. This is the policy: https://foundation.rust-lang.org/policies/logo-policy-and-me... and I don't see any deal breaker.


The use of trademark enforcement to coerce conferences into certain policies (which might contradict local law) is a deal breaker. It also demonstrates a willingness of the foundation to use wield power for purposes completely unrelated to the language.


There is no such things about forcing policies for conferences in the current trademark policy. (You're getting confused with a proposal that is being re-worked)

And even if there was it's hardly a deal breaker to use the language.


The proposed policy, which is clearly political in nature, has resulted in our firm decision not to adopt the crab language for our projects. Although Rust™ offers valuable features, introducing politics into the equation was both unnecessary and has caused a rift that may prove difficult to mend.


Good points. I like C, I think simplicity and hardware alignment make it very useful. Simplicity wins in the end.


C is not aligned with modern hardware and optimizing compilers are severely hampered by things like undeclared pointer aliasing.


so then, add a declarator or two to solve the problem, don't just throw the baby out with the bathwater. The population of C programmers who understand how C works is the population of people who can understand how hardware works. The reverse has never been true, hardware designers have never understood software, and academic software experts have spent too much time babysitting college freshman who can't code and spend all their time dreaming about languages that freshmen could learn well enough to TA the course. But that's probably not a language good enough for systems programmers to adopt.


It is called restrict and usually a fun problem to debug if one causes UB by actually having two restrict pointers doing aliasing.


> C is not aligned with modern hardware and optimizing compilers are severely hampered by things like undeclared pointer aliasing.

Which language is?

I mean, if you're going to parrot this line, surely you have an example of a language that is more closely aligned to hardware than C, right?

I see this line repeated in every HN thread about C. It's not a new sentiment, but it is mostly wrong because the implication is that there exists some other popular language that actually aligns with the hardware better than C does.


A language can not be exactly aligned to every possible hardware variation at the same time of course. So C does dome abstracting from the hardware level but keeps the mental model of memory locations which can be pointed to by pointers.


> A language can not be exactly aligned to every possible hardware variation at the same time of course. So C does dome abstracting from the hardware level but keeps the mental model of memory locations which can be pointed to by pointers.

Sure, and I agree with you, but anyone complaining that the language which models hardware more closely than any other language, "doesn't model the hardware" should be prepared for the followup question of "Well, which other language in common use is closer to the hardware?"

It's really tiring reading the same old assertions in every thread about C; usually backed up by the same two or three sources that everyone has already read.


We have entire OSes built in rust, so that' not the case there. That's both for mcus and desktop machines and beyond.


Unless that ABI is something like COM or WinRT.


we don't have standard ABIs for either C or C++; we have conventions.


Counterpoint:

https://wiki.osdev.org/System_V_ABI

The System V ABI is closer to being a standard than some official standards are.

C++ gets odd with name mangling, but the platform's C ABI is typically the tune every other language must dance to for its FFI.


> The System V ABI is closer to being a standard than some official standards are.

That brings back some unpleasant memories. What happens when a popular compiler decides that whatever they choose is the ABI, and the published spec be damned? https://gcc.gnu.org/bugzilla/show_bug.cgi?id=38496


Counterpoint,

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

https://os.mbed.com/docs/mbed-os/v6.16/apis/platform.html

Not every OS is written in C, thus the platform ABI isn't always C.


Some people like to workship whatever the UNIX founders have done, yet they miss that for Plan 9 and Inferno, they also decided to go with automatic memory management languages.

While Alef failed, Limbo's design with GC was considered a revisit from what was missing from Alef.

They also miss that lint was considered a must have tool for safer C code, introduced in 1979, and that Dennis actually proposed fat pointers to ISO, which weren't accepted.

https://en.wikipedia.org/wiki/Alef_(programming_language)

http://doc.cat-v.org/plan_9/2nd_edition/papers/alef/

http://doc.cat-v.org/inferno/4th_edition/limbo_language/

https://www.bell-labs.com/usr/dmr/www/chist.html

https://www.bell-labs.com/usr/dmr/www/vararray.pdf


> yet they miss that for Plan 9 and Inferno, they also decided to go with automatic memory management languages

And which of these three operating systems won?


Operating systems don't "win" because they make the best technical choices ... or even because they are best for their users.


Maybe, but when all the winning OSes share a common characteristic then you cannot simply dismiss that characteristic as irrelevant to winning without some evidence that it is not relevant.


The winning characteristic in this case is "it was already there, and it was good enough". This is a recurring theme in the industry.


"Plan 9 failed simply because it fell short of being a compelling enough improvement on Unix to displace its ancestor.

Compared to Plan 9, Unix creaks and clanks and has obvious rust spots, but it gets the job done well enough to hold its position.

There is a lesson here for ambitious system architects: the most dangerous enemy of a better solution is an existing codebase that is just good enough."

Source: https://www.catb.org/esr/writings/taoup/html/plan9.html


The free beer one, because everyone likes to get source tapes for free, regardless of the quality, free usually wins out.


This doesn't seem to hold in the general case. Windows and Macos are not free in any sense, but vastly outnumber linux on the desktop.


The desktop is not the general case.

By install count, Linux runs on far more platforms than anything else: smartphones, tablets, servers, virtual machines, embedded devices, supercomputers, and spacecraft.


They may not technically be free, but they typically come bundled with the computer you buy. Linux has yet to be shipping with free comparable computers, so the cost of Windows/MacOS is invisible to the end consumer.


Because Linux folks cannot get their act together in what means a full stack desktop experience.

Not even Android games get ported in any significant number to GNU/Linux.

The war of Linux Distributions is ten fold worse as the UNIX wars.


C programmers are implicitly converted to the required type.

Implicit conversions are one thing I don't like about C, or at least the way it ended up as int grew (maybe it would have been better if ANSI had picked unsigned-preserving, but maybe that's just grass-is-greener thinking). I also don't like how UB turned into “a license for the compiler to undertake aggressive optimizations that are completely legal by the committee's rules, but make hash of apparently safe programs.”

I do like the directness and explicitness. Good C can be very nice.


> I also don't like how UB turned into “a license for the compiler to undertake aggressive optimizations that are completely legal by the committee's rules, but make hash of apparently safe programs.”

You can have a language where all the behaviour is defined but in my experience most programmers who whine about this in C are just as unhappy because what they actually wanted was DWIM. In a language with defined behaviour their program is wrong, which is their fault, whereas in C they can claim "obviously" it's the compiler's fault.

I like it when it's my fault, I just learned something, and early in my education I assumed everybody was the same but I rapidly learned that most people prefer to be told they're correct regardless, and a language which says "Nope, 4 - 5 is a negative value and this variable in unsigned, too bad" makes those people unhappy, whereas when 4 - 5 = mysterious program behaviour they can blame the compiler engineers instead.


4u - 5u is not undefined behavior in c; unsigned arithmetic overflow is defined to wrap around, in both directions

assigning -1 to an unsigned variable has never been undefined behavior either, but implementation-defined behavior (see §6.2.1.2p3 in iso c90 or §6.3.1.3 in c99 and c11); decent compilers like gcc define it to do the obviously correct thing (see https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.h...), not produce mysterious program behavior. in the new c++20 standard it's no longer even implementation-defined, but fully define behavior in the standard, but in standard c it's still implementation-defined

as a programmer who whines about the kind of ub exploitation at issue here, and who (as established above) has a much clearer idea than you do of what behavior is and is not defined, specifically what i am whining about is that it's harder to debug my code if doing things like dereferencing a null pointer doesn't reliably crash it, but instead triggers the silent removal of apparently unrelated logic from my program, or subtle semantics changes in it, in the name of optimization

also it sucks to have existing security-sensitive code acquire new security holes, and i don't really care if that's a learning opportunity for the guy who wrote it 20 years ago

this didn't happen 20 or 30 years ago, and in those 20 or 30 years the impact of introducing fresh security holes into existing well-tested c code has greatly increased, while the usefulness of c compiler optimizations has greatly decreased


You're correct that 4 - 5 isn't undefined in C. But there's a good reason that Rust, for example, chooses not to make its built in integer types (either signed or unsigned) wrap on overflow. Wrapping<u32> is very easy for a typical processor to implement but in most cases overflow is a mistake, so you want to report it not silently give the wrapping answer instead when the programmer apparently hasn't considered overflow at all.

If you give up the aggressive optimisations then C is even less competitive with modern languages, so you're basically arguing that it's acceptable to have code that's harder to write, more buggy and with worse performance just because that's how you'd have done it 30 years ago. At that point you're talking about a retro hobby.


what i'm arguing is that c compilers exist primarily to support the base of existing c code and to run it correctly and with adequate performance, not out of some unhinged notion of 'competitiveness' or to entice people into writing new programs in it

that is, i do think it's acceptable to have code that's harder to write and with worse performance, but not because of some hypothetical; rather, it's because that's how people did do it 30 years ago, and 20, and sometimes 10, and i want to run the code they wrote. this could be described as a 'retrocomputing hobby' except that that code is, for example, linux, cpython, gcc, r, libreoffice, gtk, glib, grub, bash, freetype, emacs, vim, libjpeg, libpng, postgres, mariadb, ssh, openssl, gnutls, apache, poppler, ghostscript, tex, gpg, zlib, memcached, redis, etc.

if you want to describe running vim and bash and cpython on linux as a retrocomputing hobby, i guess that's kind of defensible actually, but it comes across as wishful thinking when we don't have a working non-retro alternative yet

(moreover i have no idea why you think omitting these risky optimizations makes c code 'more buggy')

i agree that there are numerous deficiencies in c's semantics, and plausibly silent wraparound is one of them, though it seems like rust's alternative is that overflow crashes in debug builds and silently wraps like c in release builds


I think you need to reevaluate what happens to code once optimizations are turned on. Especially if you can afford to do test guided PGO combined with LTO. There is nothing in common between how code is executed with optimization and what were programmers writing years ago. It is like 5-10 levels of functions are just gone and completely restructured by compiler. I do not see middle ground if people want to keep the performance. Old C++ and likely C that failed to optimize is just gone at the moment.

Whatever legacy code has to be "preserved" is likely to be specifically deoptimized to keep it in frozen state.

PS I come from gamedev and I really wish compilers left us control of what is going on there.


inlining code and most code-movement optimizations are among the many optimizations that don't require the kind of nasty tricks with undefined behavior that i'm criticizing, and it's been common practice for decades; c++ has depended on method inlining to get acceptable performance since the 80s

it's kind of a pain if you're stepping through the code in gdb but that's acceptable

turning off optimization entirely is still too costly for most cases


That is an interesting point. I wonder if it would be feasible to go over every little bit of UB in the Standard and see if it could instead be well-defined in a way that would be compatible with most existing C code out in the wild. How much perf overhead that would actually be? And is there a way to quantify how many exploits that would have prevented?


this was dan bernstein's project with 'boring c' and john regehr's with 'friendly c'; i think the answer is that most of it can be, but definitely not all, and possibly on some platforms it wouldn't be most existing c code

https://blog.regehr.org/archives/1287

basically it seems feasible but it's going to be a lot of effort

also of course any semantics change will introduce some new exploits, but doing that once seems preferable to doing it every year


To be clear, I'm thinking specifically from the perspective of making existing legacy code less broken, not about making the language "more friendly" - as you rightly point out, C really ought to be considered legacy by now (and in new languages, where we will also have this discussion, we can at least begin it from a clean slate). Thus there's no need to consider usability. I was also imagining a hard rule: if the only reason why something is UB is perf overhead, it must not be UB (this covers e.g. the memcpy/memmove debate brought up in that article). But, yeah, as the other example with shifts illustrates, even such tight-fisted approach can be problematic.

The real question is how much perf overhead this entails vs how much benefit from mitigating yet-undiscovered issues caused by UB in legacy code.


C already has bad performance due to null terminated strings.


no


The original intent was that “Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose.” If you wrote ‘a + b’, you could expect your hardware's add instruction, and if that happened to trap on overflow, that wasn't the compiler's fault.


I used to think implicit type conversions were bad in C, until I started programming in javascript.


`5 - "2"` is 3, and `5 + "2"` is "52". Makes perfect sense! It could be worse, though; you could be doing Norwegian YAML.


> C programmers are implicitly converted to the required type.

stop trying to coerce me


Consider it a promotion.


Or a type of pun


The post is kind of confusing regarding which sort is the "first sort", because they are presented in one order in the provocative thesis, and in the other order in the following paragraphs.

But:

> My perception of the second sort of C programmer is that if they've moved to any more recent mainstream language, it's probably Rust.

If in this context the "second sort" is the type who likes C on its own merits (rather than because they don't have any other choice), I think Zig is a much better option than Rust.


I am the first sort. I mostly like the ideas from C but don’t like the actual implementation. Which is why I’ve moved on to Zig.

The author asserting the second sort would have moved on to Rust makes sense. They are ones who are trying to get away from C ideas


I've always thought of myself as the second type of C programmer... I only used C if no other option was available (yeah, it was indeed fear!). After I started programming in Zig, I realized I really appreciated all of C's simplicity and (lack of) features, but now in "Zig fearless mode", it makes me feel like I always belonged to the first group.


I don't get enjoying every second line being "if (r == -1) goto cleanup" and then a cleanup block freeing every pointer which is not null. True, you can define macros to automate that, but then you just end up with clunkier version of C++ destructors. The rest depends on what you are doing, no need for heavy OOP for simple tasks and on the other hand automatic memory management is great when reliability is more important than hardware access or real time performance. But at least no reason to avoid automated facilities that replace well known red tape.


I appreciate the sentiment, but:

- there doesn't have to be "two kinds". Trivially you can fit both "types" at once, I certainly feel that way. And if there are 100 programmers, there'd be about 237 other reasons to use a language. False dichotomy.

- a better categorization might be "there are two kinds of C programmers: those who eventually start using rust and those that don't." which is at least of course absolutely true.

- I dislike the idea that a programmer is bound to a language. He's a "C" programmer, She's a LISP person, etc. A good software engineer should use about 5-10 different languages appropriate for the purpose at hand.


I disagree that enjoying C automatically means you will like Rust. If you are obsessed with safety, Rust will be a nice solution. But Rust is a much more complex language than C, which will turn many C programmers away.


But its not really.

Sure it seems so when you start. K&R and away you go, that programming is a lot easier than writing rust sure.

But unless you are writing for just yourself, no current C software get developed that way. You need to understand C standard, that is quite complex and you need to understand it in detail because of undefined behavior (and of course how your compiler interprets it and sometimes fight with compiler to emit right code).

Then you needed to know all the gotchas in the standard library, and how you properly use them. Then you need to know how to do basic arithmetic without having undefined behavior (that's where a lot of memory leaks happen). Or maybe the software you are working on has created their own implementation of most of those functions, so you need to figure how to use those. Even to just print out a debug statement can sometimes be nontrivial.

And of course you need a way to compile the damn thing, just figuring out what sane options you need to use to get decent warnings and what they mean takes a while.

And when you figure all of that out, you still need to keep track of most of the stuff that borrow checker does, but this time by hand using comments and conventions (that you also need to learn for each project.)

(I left out whole build setup, but its usually harder than Rust one too. )

I believe that programmer who has only programmed java or maybe C# before (or python, js...) will get up too up to speed faster on Rust than on C on most real-world projects

C is deceptively simpler.


C is simpler so long as you avoid "smart" code and stick to simple patterns consistently, like if/goto error cleanup or using libraries to deal with strings. It's not hard, just very tedious. Someone coming from Java or C# would quickly recognize that this is basically doing what they've taken for granted, but manually. Once they do, so long as they have the discipline and patience to stick to this approach, it's not really all that different. GObject is a good example of this kind of C.

With Rust, you have to think very differently about how to e.g. structure your data to make the borrow checker happy in many cases. This doesn't really translate to anything similar in other mainstream languages; it's a whole new skill that has to be learned.


I think you might not have read my bullet point that mentioned rust very carefully.


> I dislike the idea that a programmer is bound to a language. He's a "C" programmer, […]

Nothing in the article suggests that being a "C programmer" is that kind of a binding label. It's you reading that into it ;). (Though certainly influenced by the fact that that is a widespread interpretation.)

FWIW, I consider myself a C programmer, and in both camps presented in that article. But I'm also a Python programmer, and I don't think either of those two languages "owns" me.


Dunno, I'm a self-professed "java" programmer even though at my current and prior job there is not a single line of Java code. (Current job is golang and python, prior job was python and scala).

But many of my side projects are Java and she is always first in my heart no matter where the money takes me.


“My perception of the second sort of C programmer is that if they've moved to any more recent mainstream language, it's probably Rust.”

If you are the first type, you likely get the second type’s taste wrong. Rust is very very different from C both in terms of syntax and philosophy.


Rust is from a different language family, but I think it still fits a taste of "low-level control, not OOP, not C++".

I know many people see "complex with angle brackets" and equate Rust more with C++, but I disagree and think it's still closer to C — I can convert C libraries 1:1 to Rust, but C++ libraries hit an impedance mismatch and are really hard to rustify.


> "low-level control, not OOP, not C++".

Things have a drop-scope. There are hidden initializations. Boxing is required for many things including dynamic dispatch in error handling. The tendency to use FP-like one-liners. All that mixed with funny syntax that hurts C developers eyes.

A lot of "no-go" in C land (at least for me), that makes really hard to learn and to move definitely into Rust (again, at least for me)


The use of macros and compiler plugins is pretty much closer to C++.


I feel like another division is there are programmers who love mathy languages and syntax and those that hate mathy languages and syntax. The former looks down on the latter with contempt. And the latter thinks the former is annoying.


I'm the former kind: I choose C because I like it above all else. [1]

That said, like the author, I'm trying to find an alternative. Well, more like I'm building my alternative because I hate Go. And Rust. And anything else.

I don't know why C fits my brain, but it does. I think it's because my brain is low-level; I like messing with assembler when I get the chance to optimize.

[1]: https://gavinhoward.com/2023/02/why-i-use-c-when-i-believe-i...


>I have special array types that I use instead of straight pointers, and those arrays store their bounds.

You cheat. You don't write C the hard way. But ok, that's an easier way to use C today.


Okay? I mean, I have the discipline to admit I need to do that and the discipline to actually do it.

I'm okay with cheating if it reduces bugs.


What type of projects do you work on?


A compiler/interpreter, build system, init system, version control system, among others.

It's all in a monorepo that has my "libc".


How do you feel about zig?


I hate it.


Care to explain why?


Several things, but I'll name two.

comptime just doesn't fit my brain well.

Async Zig gives me a headache. [1]

[1]: https://gavinhoward.com/2022/04/i-believe-zig-has-function-c...


Got it. Thank you for your perspective. I can appreciate that comptime is tricky. Indeed, async zig is hard and annoying (but you're wrong about the function coloring... Here is someone's project where they call an async function alternately from a sync or async context with zero lines of code:

https://youtu.be/lDfjdGva3NE?t=1999

Yielding is async and threaded is sync)


I'm not wrong.

He claims that you can call functions fine in either mode if the functions are known at compile time. I never disputed that.

If he tried to call those functions with possibly unknowable function pointers at runtime, I doubt it would work as well.

There is a difference between compile time and runtime. At compile time, Zig can transform known functions to async to allow turn to call async functions. At runtime, that's not possible.

The 75 examples in my post show that.


sure, a sync function and an async function get compiled to two different things under the hood, but IMO colored/uncolored is about ux, not what happens under the hood. In go, or erlang, which don't have async coloring, you don't actually care what the system does under the hood. More generically, if a system takes a private function and can choose to inline it, you probably wouldn't moan that under the hood it's doing something different and thus "inlining is function coloring". Maybe you would.

I guess the question hinges on "just how hard" counts as "hard" for #3. I would say "not having to write an event loop because the compiler generates another version for the sync-calling-async with a single keyword" case counts as "not hard", but this is a matter of perspective. The language design still has rough edges, and you certainly ran into them, but with the function pointer stuff, I think you were really trying hard to break it to prove a specific point (this is a good thing! it will allow the team to fix it). To that point, they simply hadn't run into the specific runtime/comptime thing that you ran into, but it's pretty easy to see how exactly what you ran into can be addressed and fixed at comptime, it... just hasn't yet.


It can't be addressed at compile time. That's the whole point of the uncomputability of the Halting Problem.

The Zig compiler would have to have perfect knowledge of all functions that can be called anywhere. At compile time. Alan Turing proved that this is impossible.

In other words, Zig cannot address it. Ever.


Ok? The universe of all code that can be created by the zig compiler is strictly bounded for any given project at any given time, the halting problem does not apply. At worst, you can do an exhaustive search. The context that you're running the code in can ultimately be known, and the pointer can be set to the appropriate version based on calling context.


No, you can't do an exhaustive search.

Say you're a library author. You distribute pre-built library files.

You've already lost because the compiler does not know your clients' code when it compiles your library.

I am not kidding when I say it's provably impossible.


"pre-built library files" are not a thing in zig right now, unless you use the c abi, which doesn't have async?

Even supposing zig had prebuilding someday, you would probably have to extern it using async directly declared in the callconv (.async is an option in callconv) and so yes, the compiler would know. It's just that currently zig has a lot of "let the compiler figure out". Maybe that's what's making you uncomfortable?

I'm pretty sure the zig compiler is finite and deterministic, so sophistry around the halting problem doesn't apply.


Wat.

The Zig compiler is finite and deterministic, yes, but that's not the problem.

The problem is that user code is not finite nor deterministic.

Even worse, Zig's compiler may be deterministic and finite, but the language is not, by far. In fact, the language is one of the most infinite languages in existence because just the type system is uncomputable! [1]

(In this context, the word "undecidable" used by that source means "uncomputable".)

If the type system is uncomputable, then the type system will never be able to resolve all uses of function pointers everywhere.

The whole point of the Halting Problem is that you could have a finite and deterministic Turing Machine that runs other Turing Machines, and that finite and deterministic Turing Machine could run into situation where it never halts.

I never say this when people ask (because most people don't understand), but the real reason I don't like Zig is because of how infinite the language is, starting with its uncomputable type system.

This makes Zig one of the most unreasonable [2] programming languages in existence.

If you don't believe me, ask the Zig team how they plan to solve this problem. Don't ask them if they plan to solve it; ask them how.

Then if they reply, email me [3] with their proposed solution, and I will destroy it. I will do that no matter how many solutions they come up with because math says I will always win.

But if they say they can't, will you believe them?

[1]: https://3fx.ch/typing-is-hard.html#zig

[2]: https://fsharpforfunandprofit.com/posts/is-your-language-unr...

[3]: https://gavinhoward.com/contact/


> If the type system is uncomputable, then the type system will never be able to resolve all uses of function pointers everywhere.

Can you elaborate on what you mean here, and the problems this might cause? Do you mean that some function pointers cannot be resolved to concrete functions? Or that the process of evaluating comptime may be infinite? Or some other problem where the compiler can't determine whether a function pointer is needed for a given function?


These are great questions.

All of the above, actually.

Turing-completeness and the uncomputability thereof are widespread, easy to build, hard to keep away, and affects everything that might happen at runtime.

A list of things that can happen is infinite, but that list absolutely includes:

* Whether a particular function pointer resolves to certain functions,

* Whether evaluating the comptime portion of a Zig program will take forever,

* Whether the compiler fails to exclude a particular function from being used as a function pointer at a particular place.

Turing-completeness is a wildly powerful property, but that power is not free; the inability to figure out what might happen in a program is only just the biggest and most apparent cost.

The problems this might cause can be summarized like this: you can never know what a program will do for a given set of inputs until you run the program on those inputs, and even then, you might never know if the program never halts.

Does that make sense?


> Whether a particular function pointer resolves to certain functions

This is only a turing completeness issue if the list of functions is infinite. It is not, in zig.


You don't need an infinite list of functions to run into the problems with Turing-completeness. You only need effectively infinite behavior, which means all you need is effectively infinite possible inputs.


this is simply untrue. I'm sorry. I recommend studying math a bit more rigorously before making claims in the future.


> If you don't believe me, ask the Zig team how they plan to solve this problem. Don't ask them if they plan to solve it; ask them how.

The Zig compiler has a counter that describes the maximum allowed amount of branching that the compiler can do while evaluating comptime code. The counter can be manually set to a higher value but all compilations will eventually fail if your comptime logic is too loopy. In practice the guideline is to not overuse comptime and instead create a dedicated build step for things like creating pre-computed value tables.

> Then if they reply, email me [3] with their proposed solution, and I will destroy it. I will do that no matter how many solutions they come up with because math says I will always win.

cringe


> The Zig compiler has a counter that describes the maximum allowed amount of branching that the compiler can do while evaluating comptime code.

Okay, here's a solution. It's already implemented. Nice.

Now let me destroy it.

You said those compilations "fail"; I presume there's an error that kicks in.

In that case, then some valid Zig programs will be rejected, but your type system is now not technically Turing-complete. It's also less powerful; there are going to be things that Zig's type system cannot express.

But wait; that's a user-defined setting? This means that either users will increase that setting when they need to and run into the same problem, or follow your guideline:

> In practice the guideline is to not overuse comptime and instead create a dedicated build step for things like creating pre-computed value tables.

It sounds like a dedicated build step would be the better option than comptime, to be honest.

Also, if you step out to dedicated build steps, then the compiler no longer has complete visibility into that code when analyzing. At that point, the Halting Problem applies again.

Also, what do you do if a user needs to pass a function pointer to a C function? I presume they must use a sync function? If so, that seems like a direct application of the function colors definition; some functions are unusable in certain places. If not, I'd be interested in what Zig does there.


> In that case, then some valid Zig programs will be rejected, but your type system is now not technically Turing-complete. It's also less powerful; there are going to be things that Zig's type system cannot express.

I think you're a bit too high on all this compsci theory stuff.

All computations in all languages (including ones generally considered to be turing complete) fail when you run out of resources like RAM or patience. Zig introduces an artificial quantity that can be used as a machine-independent way to ensure all compilations eventually end in either success or failure. It's like a timeout, but an actual timeout would be inconsistent across different machines (with different performance characteristics).

The actual state of things is much more boring than what your analysis tries to suggest: comptime is used to do some metaprogramming whose practical value is in good part determined by it not being too crazy, so the fact that you can't make a DisprovesCollatzConjecture type is actually fine.

You have found another feature of Zig that's even more mundane than async where you're trying really hard to misunderstand everything to indulge in... "destroying".

I think your own wording betrays the fact that you care about needlessly debating people more than actually understanding the tools in front of you in order to produce better software.


Comptime is too crazy.

Comptime is powerful, but without that counter, it's effectively uncontained.

With that counter, it's contained, but then some comptime stuff needs to be separate, where users lose the benefits of Zig's all-at-once compilation model, a model that is necessary because of the feature of hiding function colors.

If you think I care about "needlessly debating people more than actually understanding the tools in front of me," let me dispel that notion.

I was once a fan of Zig.

Yep. I was. I thought it was great and was even going to rewrite all of my monorepo in it.

And then I started studying it in detail, trying to understand it. It took a while, but I came to realize that Zig appears good and has flashy ideas, but that Andrew just didn't understand the consequences of his design decisions.

In other words, I spent time trying to understand Zig before I ever got these opinions.

To go back to the topic, sure the use of this may appear boring, but if tools have a hard time working with Zig code, so will people. That's my problem with it.

You are absolutely right that this is a "boring" part of Zig, but that doesn't mean it's good. It means that that's where complexity hides.

Once I figured that out, I realized that to make better software, it would be better for me to stick with C.

My wording, by the way, comes from a bit of exasperation because to me, it feels like you, Andrew, and the rest of the Zig team are reluctant to accept criticism or that Zig isn't perfect or even the best.


> then some valid Zig programs will be rejected

A program is defined to be invalid when it runs out of counters. Problem solved.

>Also, if you step out to dedicated build steps, then the compiler no longer has complete visibility into that code when analyzing.

You can't really "generate code" in these dedicated build steps. Kristoff is talking about generating data. you could maybe build an object file, but then it's bound in using c abi, statically, which we've already solved in this conversation.

>Also, what do you do if a user needs to pass a function pointer to a C function? I presume they must use a sync function? If so, that seems like a direct application of the function colors definition;

In the long run for zig, calling out to c abi, much less one which takes a function pointer, much less one which you happen to have as async, is going to be a rare enough operation that I think people aren't clamoring to go and use it, which fails one of the criteria for your function coloring list. Besides, you just quickly wrap it in noasync context and you're done (you can't pass a zig callconv function directly to a c abi callconv site without at least a tiny wrapper anyways).


> A program is defined to be invalid when it runs out of counters. Problem solved.

Except that the counter can be adjusted by users.

> You can't really "generate code" in these dedicated build steps. Kristoff is talking about generating data. you could maybe build an object file, but then it's bound in using c abi, statically, which we've already solved in this conversation.

So any code generated outside has to be sync? That's exactly what I thought.

But even if Zig only allowed people to generate only data, this is a pretty big limitation.

This may not be a problem now, but it does mean that as Zig programs grow, people will run into this problem. I suspect that this means that Zig will struggle more than C to scale, maybe even more than C++ or Rust.

> In the long run for zig, calling out to c abi, much less one which takes a function pointer, much less one which you happen to have as async, is going to be a rare enough operation that I think people aren't clamoring to go and use it

This assumes that people won't want to interface with software that isn't in Zig. This is another way that Zig will struggle to scale.

> Besides, you just quickly wrap it in noasync context and you're done

This sounds exactly like a special calling technique mentioned in the function colors post.

> (you can't pass a zig callconv function directly to a c abi callconv site without at least a tiny wrapper anyways).

And this also sounds exactly like a special calling technique.


> Except that the counter can be adjusted by users.

So? You've changed the program, and now it's valid. (as my handle suggests) I was a math major: arbitrarily large but finite is a different category than infinite, and that makes all the difference in the world.

> his sounds exactly like a special calling technique mentioned in the function colors post.

No, but the seam between async and sync in most other implementations requires writing an entire event loop. In this case it's often a single keyword, six characters. At worst, you write an extra function header to wrap it.

It's not the extraness that matters, it's the pain of doing so. Hell, even go requires you write "go" in front of your function to run it asynchronously, and I don't think that this counts as "colored functions".


it's not possible for human beings to write correct C code, measured over time

this is not a controversial statement, it's the clear conclusion from any evaluation of available evidence

it's fine that you like messing with assembler, but you can't do that safely -- if the programs you write don't need to be correct then carry on, but if they do need to be correct, then you have a professional obligation to use a higher-level language, with stronger guarantees

edit:

> This brings me to my next reason: I have the discipline to write C at a high level.

factually, you do not. nobody does. you think you do, until you don't. human cognition is insufficient to satisfy this requirement. "discipline" does not fix the problem.


> it's not possible for human beings to write correct C code, measured over time

I don't disagree [1], but remember that Rust can be unsafe too. Async is not a panacea, and it's confusing. And the `unsafe` escape hatch is still unsafe.

> you have a professional obligation to use a higher-level language, with stronger guarantees

Oh? So we have professional obligations now? For FOSS? News to me.

I don't get paid for my work. Not yet. So I have no obligation.

And when I do get paid, my code will be in my own safe language.

This accusatory response is exactly the kind of thing that puts a lot of people off Rust.

> factually, you do not. nobody does.

When I say "write C at a high level," I don't mean that I'm perfect. I mean that I write excellent C compared to all C programmers out there.

Engineering is not about perfection; it never was. It's about doing the best we can with the tools that we have.

[1]: https://git.gavinhoward.com/gavin/bc/src/branch/master/MEMOR...


Ask any experienced C developer and they'd all say "I write excellent C compared to all C programmers out there."


I'm an experienced C developer and I wouldn't say this. I'm pretty meticulous, I use as much tooling as I can, and I've still written over/underflows, memory corruption, threading heisenbugs, double frees, NULL derefs, etc. etc. etc. My defect rate has gone way down over the years, but that's probably 50/50 experience vs. an attenuation of my ambitions.

I think the proof is in the pudding here. I can't imagine using a messaging client or an email client written in C, and also there isn't much that is written in C anymore even if I wanted to: you're looking at Swift/Java/Kotlin/TypeScript/JavaScript/C#/Vala/Dart/Rust/Go, and maybe C++.

Is anyone writing production servers in C these days? It feels like they're 100% Go and Rust.

There are notable exceptions, in particular I'm thinking about OpenBSD. I think OS dev is a pretty niche area though, especially when you're talking about OpenBSD's wide platform support, so I think it's more the exception that proves the rule than anything.


That is true. I'm repeating what others have judged and told me.

Of course, feel free to judge for yourself.


This could be true if you consider survivor bias.


Oh wow, someone should alert the Linux kernel maintainers. Do you want to tell them that it’s impossible to write correct C code? And the rust compiler team, too. After all, if nobody can write safe assembly then whatever they’re doing is either unsafe or magically gets the computer to understand rust directly. Or are they relying on LLVM for their code generation? I forget what language that’s written in, but nothing “unsafe” happens there surely.

All code is machine code at bottom. Including the code that maintains abstractions convincing enough for you to think the “memory-safety” of rust or any other language is a static and guaranteed thing and not something that needs “unsafe” scaffolding to support it.


i'm sure the linux kernel maintainers already know that it's impossible for them to write correct C code, no need to tell them


The Rust compiler developers are well aware that it's not possible to write 100% correct code at scale, not least for their dependencies, LLVM provides Rust with ICE (cases where your program crashes the compiler) and with soundness holes where the LLVM behaviour is definitely wrong but it's arguably exactly why and so until LLVM developers decide why it's wrong and fix it, Rust has to either work around that or accept sub-par results.

Much of the Rust compiler is written in Rust and so has fewer problems.



If the existence of C cves in the kernel proves that it is impossible to write correct C, then by the same token any cves in any rust code prove the same thing about rust. This is such a lazy way of arguing. Say something about why the tradeoffs favor a more restrictive and less performant language or don’t, but don’t dismiss the work of many thousands of C developers that runs most enterprise systems with a knowing wave of the hand - it’s not serious.


So you want something more serious,

https://msrc.microsoft.com/blog/2019/07/a-proactive-approach...

https://security.apple.com/blog/towards-the-next-generation-...

https://www.chromium.org/Home/chromium-security/memory-safet...

https://security.googleblog.com/2022/08/making-linux-kernel-...

Σ (memory corruption) + Σ (logic errors) ≥ Σ (logic errors)

So by reducing in 70% the costs of fixing the errors caused by those thousands of experts, as validated by the above reports, there is already a considerable reduction in software development expenses.

Lets see how serious those developers get to be around security issues, when liability finally takes off.


I appreciate that you are making an argument and will respond when I get a chance later in the day and after I read a few of the links - I’m familiar with like half of them.


it is not anyone's claim that the existence of C cves means it is impossible to write correct C

the point that is being made (obliquely) is that manual memory management is not possible for human beings to do effectively at scale

at scale means across a broad demographic of programmers, working on a broad set of programs

if you wanted performant and portable then C was all you had for a long time, and there is a lot of good software written in C, but that does not mean that it is a good tool in perpetuity

strcpy and pointer arithmetic are basically not possible to get right

we need to move beyond this very low level of abstraction


> it's not possible for human beings to write correct C code

It's not possible for human beings to write correct code.

The hardest bug I ever found in a C program, one that took cumulative weeks of work until a tractable reproduction was found, came down to a ‘<’ that should have been ‘<=’. No language would have stopped that.

Yes, different languages have different levels of expressiveness, and can preclude or expose different kinds of errors. You'll never have a stray pointer in Python, and you'll never OOM making an accidental copy in C.


I assume this was a logic error, that is, the code as written was a valid C program that didn't do what you meant.

In that case this could have been caught with more careful unit tests, which would have exercised the erroneous condition, and potentially with fuzzing to excite corner cases. C is... not great for either of these.


I don't know why you say that C is bad for exercising error conditions or fuzzing. In my experience, it's the best at those.


It doesn't come with either capability, so you're adding third party tooling to get these working.


Well, sure, but that's true of any language.


Nope, when I write tests in my Rust the out of box cargo test will check those tests pass.

If I wrote inline Markdown documentation for part of the Rust code with an example, cargo test will try to build my example, and check it works - not much good having a purported "example" which doesn't even work, is it? And yet many languages don't do this.


Mere existence of a test runner is infinitesimal compared to your proposed careful testing. Saying this as someone who wrote my own test runner, it's a few lines of code.


You said fuzzing and exercising error conditions. Rust's tooling does not do that. You need something to generate those tests.

Fuzzing is still not part of the default Rust tooling.


Not true. Check out CompCert, seL4 etc.


obviously yes

yet c code is inarguably more error-prone than basically any other language


Nope. Many errors in JavaScript, python, PHP, etc are caught at compile time in C.

Of course, the errors that do get through might be more severe in C than in python, but not always.

After all, the most expensive and destructive RCE ever was in a piece of Java software.


What you mean to say is you have no shot at correct C, and so the language scares you, and you believe since you can't do it nobody does.

It's true that the community at large writes lots of bugs. But that doesn't mean some people aren't productive with the language and write a relatively low number of bugs. There are some projects with better track records at this than others.


no, that is not what i mean to say

at scale, it is not possible for humans to hand-write C programs that are free of segmentation fault class errors

this is not controversial or in any way arguable


Your opinion is common here. However, you can't just say your opinion is the consensus and that makes people who have other experiences wrong, end of debate. Popularity doesn't indicate truth. I'm sorry you haven't seen it working well, but there are counterexamples where people are productive with a relatively low number of issues.


Sel4 would like to have a word.

Inb4 you move the goalposts for "at scale". This is an operating system with capability-based access control which is not something that most operating systems even have.

Also the cryptographic constraints are part of the proof system.


I'm not sure Sel4 can be considered "at scale", given the rather extreme amount of effort that went into it.


you just moved the goalposts.


And yet C code runs the world. You might be right in theory but in practice C is the most successful programming language in history. At work we routinely deploy million+ lines of C in production running large international airlines and airports. And it works.


Historical accident, due to UNIX winning out the server room.

In early 1980's it was only running Bell Lab's world and a couple of universities that got hold of the source tapes.


The success of UNIX and C is no accident.


that C code is prolific is a historical fact, it does not imply anything about efficacy or soundness

"it works" is factually not true, look at ~any CVE


No successful software is bug free. Except for proven correct code like CompCert and seL4 of course.


of course, but that's not the point being made here


> it's not possible for human beings to write correct C code, measured over time

I agree that C the programming language does not help you avoid memory errors, however, saying it's impossible is too binary. People do make mistakes, whether it's copy editors missing a typo or professional chefs misremembering whether they stocked an ingredient. Projects written in C need longer periods of code review and testing, but they can be made right. The code that took us to the moon was written in assembly after all.


I am a C programmer. For me the language is a means to an end. I prefer to work on problems where both performance and code size is paramount. The kinds of problems where it makes economic sense to spend months or years of engineer time designing and implementing a solution that will compile to a 50kB binary.

I think primarily about the kind of machine code I want to generate, and pick a language that best approximates that ideal output code while still allowing the source code to be as clear as possible.

From these constraints I keep coming back to C. Zig is the most appealing contender in terms of next-gen languages.

If I could get safety without giving up anything, of course I would take it. But I am increasingly convinced that this is not generally possible.


How do you feel about Rust?


I was really positive on Rust for at least five years. But several experiences have led me to reconsider this position, most notably the difficulty using arena allocation: https://blog.reverberate.org/2021/12/19/arenas-and-rust.html

This is one of the things that endears me to Zig: passing an explicit allocator around is a very common idiom in the language.


I'm curious what you think about Ada; from what I remember, its memory management facilities have this basically as a first-class thing directly in the language.


I'm old enough to be in both camps. After years of toodling around in a variety of BASIC languages, I took a class on C. For the first time, I went from a vague text-to-result association to a clear picture of machine and its inner workings*. I then learned C++ and Java which were both hot garbage at the time, and a variety of scripting languages.

The scripting langues are great for high productivity and mediocre performance. Shell languages are great for interacting with the operating system. Query languages are great for interacting with big data. But for years, C was the unparalleled language for getting the best performance out of CPU-intensive algorithms. I've still never found a language that erases these boundaries -- while I'm a "C programmer" I'm multilingual by necessity.

Today? C's performance is matched by C++, and more tedious to write. Zig is there on performance, but for my purposes, the language gets in the way. Rust also gets in the way, and can end up forcing reference-counting which tanks performance. I do like how Go mostly gets out of the way, but again, I cannot abide unnecessary reference counting.

* and, yes, the abstraction is a lie, but it's an incredibly useful and high-performance abstraction


> Zig is there on performance, but for my purposes, the language gets in the way

I guess zig for me is indeed a slightly more complicated language than c, and it's way more verbose, but what I appreciate about it is that straightforward code looks good and code that is on shakier ground looks hairier which is basically a "this code is sus, plz review the fuck out of it" sign.

In the end though, what gets in the way for me with C is everything else to ship a "real c program", especially when you want/need someone else's code. Lexical macros, header/code separation, flat namespace, errno, architecture dependent definitions for int, char, etc, having to think about object (file) units, make, automake, config...

At that point the cognitive burden is enough that a small added complexity in the language is worth it


Go uses a tracing garbage collector. Although I suppose that's even worse from this perspective.

If you don't mind me asking, how does Zig get in the way? My own gut feel is that it's the only real "better C" out of the present crop of alternatives.


C with libdispatch and clang blocks is the most fun I’ve had programming in quite some time!

Here’s a web framework (complete with ORM) modeled on ExpressJS written in C:

https://github.com/williamcotton/express-c

The finished product is < 90Mb Docker image that idles at like 2Mb of memory.

There’s also a lot of examples of the (basically required) support tooling like Valgrind, AdSan, etc.

Check it out!


Carmack said low level programming is good for the soul, he's right. C is a fun programming language, don't even know why. Freestanding C with no standard library is the most fun I've had programming ever.

I'd like to share my project as well: a Lisp interpreter written in freestanding C.

https://github.com/lone-lang/lone


Ooh, I really like this and from a cursory glance it looks very legible, I’m definitely going to poke around later!


I would argue that Forth is really the perfect thing for such zen-like pursuits, building it up from scratch bit by bit. Freestanding, especially on some more exotic platform, is more fun, of course.


It looks cool. The DB library is appreciated. However, you would get about the same idle memory and docker size with Go's Fibers framework :)


Lol, yeah, the libdispatch runtime is probably about as big as the Go runtime!

I think this has a slight advantage in that it uses a memory arena for a per-request bump allocator so it should keep overall memory usage lower.

The slowest part of all this has to do with Block_copy and the places where I’m very much treating blocks/closures in an OOP type manner.

This could be fixed by writing a different function that did basically the same thing as Block_copy but for all the “methods” on an “object” in a single pass. But who has time for that? :D

Here’s more about those performance issues and some example code that show how slow the approach is:

https://github.com/williamcotton/express-c/tree/master/resea...


go-fiber is a weird, non-idiomatic, and non-serious project, fine for a proof of concept, but definitely not something anyone should be using in prod

but your point is sound, any reasonable go http server will have the same level of memory usage at idle


Wow, really? The project has 25k github stars and 300 contributors. They are on v2.44. Why is it not serious? What makes any software "serious" to you?


this project makes assumptions about received input (specifically encoding) which aren't guaranteed

fine for a toy project, not something that can be used in anger


Are you referring to the req->sendf as seen in the README?

Yeah, I definitely wouldn’t use that approach for user input! There’s also support for mustache templates and JSON-API endpoints as well.

Or are you referring to something else?

But yes, please don’t use this for anything serious!


Why I love C?

In a word, simplicity. A language designed by a single person. I also did program a lot in CLisp, a language designed by committee, with more than 100 different flow control directives(and people often create their own).

I loved and love Lisp as a concept and tool, but hated CLisp complex design so badly. I programmed in Arc for a while because of that.

I have created my own C compiler and interpreter. Something impossible to do for a single person in C++(C++ is way more complicated).

I also love manual memory handling in C because it lets me create my own automatic local systems much more efficient than someone else's "one size fits all" aproach.

I have also programmed mac apps in Objective C and Swift, C++, java, javascript, Perl.

I program today mainly in four languages: C/C++, Python, Rust and Clojure.

With C it is so easy to create python modules. It is trivial to interoperate from C++ with C. Clojure is a Lisp that is simple, clean design, and you could interoperate with java objects. Rust has some advantages over C, but also disadvantages(it is too bloated).


> impossible to do for a single person in C++(C++ is way more complicated)

I wrote a fully compliant C++98 compiler (Digital Mars C++).


> In a word, simplicity.

For fun, go to /usr/include and try to understand the system .h files.


This is why I've kinda built my own libc on the bare minimum that the system gives me. The POSIX and Windows API's are garbage; C the language is great.


i have written a fair bit of code using the windows and posix APIs, and they are OK - what features of these, often written by talented programmers, do you despise so much?


To pick the same example from both, I hate both fork() and CreateProcess().

Microsoft wrote "A fork() in the Road" [1] describing the problems with fork(), and they are right: fork() is too simple, it doesn't scale, it is inefficient, and error handling becomes next to impossible.

(In my code, I have the child process return exit codes from 255 on down for error handling. It assumes, probably wrongly but right enough, that most programs won't use those exit codes.)

But CreateProcess() has the exact opposite problem: it's too complex and limited because it takes a large, fixed set of arguments. It takes many lines of code to set up for it, and once you actually start the process, you have no control over it, which sucks if you need to do something with the new process that CreateProcess() cannot do on creation.

Windows does have ways of modifying running processes, which is great and sort of makes up for the limits, but heaven help you if you need to use one of those functions on the new process because you have no control. The only thing you can do is to suspend the start thread of the new process right away, do your thing to it, and resume the thread, praying that the new process hadn't created a new runaway thread before it was suspended.

(And all of that is not even mentioning that the Windows way of passing a command-line is to pass a string to be parsed, not a list of strings. My code has a function literally to take a list of strings and turn it into a Windows-compatible command-line string with all of the juicy backslashing that implies.)

The right API is in the middle: a zero-argument function to create a new, blank process (not a copy of the current process), but to create it in a suspended state, so you know it's not going to run away from you.

Then, you use Windows-style functions to change the process, before it even starts, to set it up. Once you're done, unsuspend it.

That end result gives you as much power as fork(), with the better scalability of CreateProcess(), and better ease-of-use than both. You could even have functions to map in a copy of the current process if you so wish, to implement fork() for process checkpointing!

In other words, talented programmers can implement things and make them work, yes, but it doesn't imply that they are good at design.

[1]: https://www.microsoft.com/en-us/research/uploads/prod/2019/0...


The irony is that what you describe is pretty much the traditional OO way of doing things. In C#:

   var process = new Process();
   process.StartInfo.FileName = "foo.exe"
   ...
   process.Start();


Oh, that's cool! I didn't know that.


and does your library support threads? in my opinion, MS made the better design decision - first-class support of threads over forking new processes.

but this is why we have different software architectures :-)


My library does support threads. In fact, it has structured concurrency as a theme through the whole codebase, based on OS threads.

I do agree that first-class thread support is better.


>I have created my own C compiler and interpreter.

It's relatively simple because everything is UB/UsB/IDB. ;)


> I no longer want to have to think about memory management and related issues.

In some cases efficient memory management _is_ the problem to be solved. I'm not just talking about esoteric or embedded systems here. If you're developing for modern GPU's with a modern API, like Vulkan, you must manage GPU memory yourself. With direct memory access you can do many "unsafe" things, like memory aliasing, to improve performance. "Safe" languages, like Rust, will not help you here.


Tense is very important with this article. People who chose C. Whatever these reasons were that made someone chose C, that choice may have been made 20 years ago, and the choice may pan out very different if made now. But now you have 20 years of C experience…


I was not there in those days (somewhat young). I have still chosen C, and I chose it before I had experience with it. I just like the low-level nature.


I used C for my own just in time compiler because I wanted to learn C.

I still don't "know" C or what's a customary way of doing things because I only started with C code by Martin Jacob that executes machine code in a memory buffer ( https://gist.github.com/martinjacobd/3ee56f3c7b7ce621034ec3e... ) and some Perl Compatible Regular Expression library example C file.

I want to know C enough that I can create good APIs in it and transpile to C effectively because I want to take advantage of LLVM and gcc optimising backends.


It's interesting that C has managed to remain relatively stable whilst everything else has ballooned in size. I spend most of my time writing C# and it has become quite large. I wouldn't want to pick it up from scratch now.


At the same time, people have expectations for ability now.

A beginner might want to make a web request, good luck doing anything beyond mimicry with C from code samples if they get that far.

With python, they could pull it off quickly.


C23 versus K&R C, and then there are all those compiler specific extensions.


actually, C has ballooned in size and complexity. it has come a long way from the first edition of k&r.


I am definitely the former. I didn't like C too much in 1986 but there wasn't really anything else except writing in Assembler! Having programmed in Pascal, it wasn't too much of a leap to transition to C. The C compilers at the time were buggy. 37 years later, I still write C programs. I won't touch C++.


I consider myself the latter who just hasn’t had the time to look into Rust or Zig extensively. But also, I work in safety critical application and it would be hard to shift the organization over to Rust for a lot of reasons, both technological and political.


I think there are way more types, or subtypes. The first category can be broken down into "people who like C because it's simple" and "people who use C because they need to do manual memory management" (and that can be broken down into "has not tried Rust" and "has tried Rust and didn't like it"). The second category can be broken down into "people who wrote a program in the 1980s in C and are now stuck with it" and "people who learned C in the 1980s and haven't tried to learn a new language" and "people who learned C in the 1980s-1990s and think that Python and Ruby would be too slow for their purposes (no longer true)".


I learnt C in college but C++ was emerging as the language of choice circa 1995 when I got into my first job.

This was before C++ was standardized and we had to make do with whatever MSVC 1.5 would do. It did not even support templates back then and even the highly polarizing STL did not exist yet -- in a cross platform way; The STL source itself was available from 1994, and if I recall right, the C++ compiler on Sun/Solaris supported templates etc., We were then a Sun/Solaris + Dell/WinNT shop then and had software running on both and therefore settled on the lowest common denominator of features supported by compilers on both platforms.

Heady days.

Given that environment, I was comfortable switching between C and C++.


Borland did templates since 1993, Borland C++ 2.0 and Turbo C++ had early support for them, and BIDS 2.0 changed from pre-processor macros into the templates experimental design.

MSVC was always behind until Borland management messed up.


Yes, that’s right. In fact, in college we used Borland compilers.

But my first job had standardized on MSVC. So, that determined what I used on the job.

Loved Turbo Pascal and Turbo C++ IDEs in those days. Simple and fast!


Personally, the primary reason I'm a C programmer because the main project I work on is a million lines of code, and it's C... As with COBOL, legacy code will mean there's still demand for C programmers for a fair while yet.


I also mostly use other languages these days, but there's still something about C that always feels like coming home to me. Maybe it's because it's what I used during my apprenticeship which is where the magic of programming finally clicked for me (after a failed attempt taking a programming class). Maybe it's just the simplicity and lack of rules to learn.


> Maybe it's just the simplicity and lack of rules to learn.

This is how I feel about assembler. No rules, you on your own. And it is impossible to imagine simpler language. Though I do not write asm lately.


the simplicity of c is an illusion the moment you start using pointers and define macros. nothing is harder to understand than a big C program, imo


I don't see how simplicity is lost through use of pointers. Dynamic language variable references are essentially pointers and they couldn't be simpler.


Pointer provenance.


There is (or maybe was a few decades ago) a third type. A person who was taught C programming in the school/university, because back then it was the norm to teach it and nothing else, and who does not have enough motivation or abilities to learn anything else on their own, even if C is clearly the wrong tool for what they are doing.


With C, programmers of different types (there are certainly more than two) can be combined into one operational unit by a clever manager.

Sometimes differently typed programmers will implicitly agree to adopt a uniform approach before working together on a project. Sometimes the programmers won't do this by themselves and so a manager is needed to explicitly guide them to adopt the appropriate type.

However, some types stubbornly resist conversion, and so are tricky to work with. Pointy-headed programmers tend to be surprisingly flexible, and the ones who stare off into the void are often the most useful when you need them to function effectively. Overall, managers who can understand and utilize this hierarchy of programmer types in their organization may find themselves getting promoted regularly.

Of course, many people can't stand these corporate structures and prefer the simple farmer's life, raising ducks instead. Quack!


I switch between both depending on my mood and the particular application.

For me: Rust killed C++ but not C

I have an on-again off-again love affair with C. And in its domain it’s still unmatched: low-level close to hardware where you directly need to deal with concepts that are normal abstracted away: alignment, custom layouts, type punning, pointer tagging, different types of memory, cache coherency, DMA, kernel bypass, etc etc

Sure you can do it in rust unsafe but it’s (intentionally) a world of pain.

But, when you start needing bigger abstractions or you work with a team larger than say 5, C falls on its face super fast.

But it Feels Oh So Nice otherwise.

Here’s to me hoping Rust unsafe becomes more usable and can be a proper C successor someday. But.. I’m happy to be rid of C++ :-)

(I’ve been meaning to write a blog post on this for some time)


One more thing: codegen

Getting good codegen out of Rust is 5 times harder than C. And sometimes it’s not really possible.


Many people I know (myself included) started in the second category in the 1980s and 1990s (C = God's programming language) and then moved to the first category once attractive alternatives (e.g., Go) that met our use cases became widely used.


> They feel strongly drawn to C's virtues, often explicitly in contrast to other languages

Can someone point me to which set of virtues you are talking about here? Simplicity? Value semantics and imperatives, little abstractions over what the computer OS is actually doing? Manual control over almost everything being done? Are such "virtues" safe and dignifying under an universal context of computer programming? Isn't it a bit dangerous to let such Virtues take over in modern programming dealing with heavy abstraction layers because of the yield in complexity of modern hardware?


I used C in the late 80s and 90s because it was the only viable option for many use cases. In the late 90s I got heavily involved in Java, and C usage dropped. Had some spikes of C++ work (which I despised), and then working in many different languages with no real work on C or C++ in at least 18 years or so.

What always killed me the most in C was the absolutely anemic standard library. OS’ had more libraries, but still very low level stuff.


To be a bit pedantic, those options aren't mutually exclusive. An appealing property of C is that it's easy to target almost any device (often making it the best or only option). So it's possible to like C because you like (and need) something you can compile in lots of different places.


Implicit in the article is that there are only a handful of languages capable of certain use cases. Eg, embedded, bare-metal, programming operating systems, performance-sensitive code etc. So, your options are limited (C, C++, Rust, Zig, ADA, maybe a few more?), which would drive option 2.


What about those who are stuck with it because of the huge legacy code base? They wouldn't choose it deliberately for anything new, but there isn't much they can easily do about it in the above case.

People who chose C as the best option at the time might not be even around the project anymore.


Not sure what’s provocative about “people use language X because either they like it or they have to use it”. The same could be said about every tool.


Also, wants to use C ABI in other software.


You can easily expose it from Rust as well.


[flagged]


… and those who mistake it for trinary.


10 in binary _is_ 2 in decimal. I think you've made an off-by-one error in your critique.


But it's (decimal) 3 in ternary, I think that's the point (you get a 3rd group of people if you're mistaken about the base)


Could be that I missed the joke.


In fact, 10 in any base represents the base number.


Good point, but not in base 1.


> In the unary system, the number 0 (zero) is represented by the empty string, that is, the absence of a symbol. Numbers 1, 2, 3, 4, 5, 6, ... are represented in unary as 1, 11, 111, 1111, 11111, 111111, ...

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


And those who know that "10" can be any number if you change the base.




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

Search: