Two types of C programmers
utcc.utoronto.ca
utcc.utoronto.ca
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++.
With that said: Swift replaced C (and Obj-C) 100% in the Apple world for me. Because the API bindings are as good.
C's "register" keyword has been ignored since 1990 or so.
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.
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.
I did learn D some years ago when the standard library situation was not great, but might be worth looking at again.
import core.stdc.stdio;
extern (C) void main() {
printf("Hello world\n");
}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).
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.
I found this pdf but I thought there was a different one, anyone link to that?
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.
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.
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.
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
I just wish their trademark policy relaxed some of the non-trademark related requirements making it a deal-breaker.
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.
And even if there was it's hardly a deal breaker to use the language.
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.
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.
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.
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
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.
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/
And which of these three operating systems won?
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
By install count, Linux runs on far more platforms than anything else: smartphones, tablets, servers, virtual machines, embedded devices, supercomputers, and spacecraft.
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.
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.
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.
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
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.
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
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.
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
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
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.
stop trying to coerce me
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.
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
- 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.
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.
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.
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.
But many of my side projects are Java and she is always first in my heart no matter where the money takes me.
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.
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.
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)
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...
You cheat. You don't write C the hard way. But ok, that's an easier way to use C today.
I'm okay with cheating if it reduces bugs.
It's all in a monorepo that has my "libc".
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...
https://youtu.be/lDfjdGva3NE?t=1999
Yielding is async and threaded is sync)
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.
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.
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.
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.
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.
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...
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?
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?
This is only a turing completeness issue if the list of functions is infinite. It is not, in zig.
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
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.
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 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.
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).
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.
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".
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.
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...
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.
Of course, feel free to judge for yourself.
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.
Much of the Rust compiler is written in Rust and so has fewer problems.
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.
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 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.
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.
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.
Fuzzing is still not part of the default Rust tooling.
yet c code is inarguably more error-prone than basically any other language
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.
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.
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
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.
In early 1980's it was only running Bell Lab's world and a couple of universities that got hold of the source tapes.
"it works" is factually not true, look at ~any CVE
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 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.
This is one of the things that endears me to Zig: passing an explicit allocator around is a very common idiom in the language.
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
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
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.
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!
I'd like to share my project as well: a Lisp interpreter written in freestanding C.
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...
but your point is sound, any reasonable go http server will have the same level of memory usage at idle
fine for a toy project, not something that can be used in anger
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!
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).
I wrote a fully compliant C++98 compiler (Digital Mars C++).
For fun, go to /usr/include and try to understand the system .h files.
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...
var process = new Process();
process.StartInfo.FileName = "foo.exe"
...
process.Start();but this is why we have different software architectures :-)
I do agree that first-class thread support is better.
It's relatively simple because everything is UB/UsB/IDB. ;)
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.
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.
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.
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++.
MSVC was always behind until Borland management messed up.
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!
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.
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!
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)
Getting good codegen out of Rust is 5 times harder than C. And sometimes it’s not really possible.
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?
What always killed me the most in C was the absolutely anemic standard library. OS’ had more libraries, but still very low level stuff.
People who chose C as the best option at the time might not be even around the project anymore.